'use strict';
function isEmptyLiteral(node) {
return (
node.type === 'Literal' &&
typeof node.value === 'string' &&
node.value === ''
);
}
function isStringLiteral(node) {
return (
(node.type === 'TemplateLiteral' &&
node.parent.type !== 'TaggedTemplateExpression') ||
(node.type === 'Literal' && typeof node.value === 'string')
);
}
const ignoreKeys = [
'range',
'raw',
'parent',
'loc',
'start',
'end',
'_babelType',
'leadingComments',
'trailingComments',
];
function astReplacer(key, value) {
return ignoreKeys.includes(key) ? undefined : value;
}
function isEquivalentCode(node1, node2) {
return (
JSON.stringify(node1, astReplacer) === JSON.stringify(node2, astReplacer)
);
}
function isDescendant(node, maybeParentNode) {
let parent = node.parent;
while (parent) {
if (!parent) {
return false;
}
if (parent === maybeParentNode) {
return true;
}
parent = parent.parent;
}
return false;
}
function isSafeTypeofExpression(originalValueNode, node) {
if (node.type === 'BinaryExpression') {
if (node.operator !== '===') {
return false;
}
const {left, right} = node;
if (left.type !== 'UnaryExpression' || left.operator !== 'typeof') {
return false;
}
if (!isEquivalentCode(left.argument, originalValueNode)) {
return false;
}
const safeTypes = ['string', 'number', 'boolean', 'undefined', 'bigint'];
if (right.type !== 'Literal' || !safeTypes.includes(right.value)) {
return false;
}
return true;
} else if (node.type === 'LogicalExpression') {
if (node.operator === '&&') {
return (
isSafeTypeofExpression(originalValueNode, node.left) ||
isSafeTypeofExpression(originalValueNode, node.right)
);
} else if (node.operator === '||') {
return (
isSafeTypeofExpression(originalValueNode, node.left) &&
isSafeTypeofExpression(originalValueNode, node.right)
);
}
}
return false;
}
function isInSafeTypeofBlock(originalValueNode, node) {
if (!node) {
node = originalValueNode;
}
let parent = node.parent;
while (parent) {
if (!parent) {
return false;
}
if (
parent.type === 'IfStatement' &&
!isDescendant(originalValueNode, parent.alternate)
) {
const test = parent.test;
if (isSafeTypeofExpression(originalValueNode, test)) {
return true;
}
}
parent = parent.parent;
}
}
const missingDevCheckMessage =
'Missing DEV check before this string coercion.' +
' Check should be in this format:\n' +
' if (__DEV__) {\n' +
' checkXxxxxStringCoercion(value);\n' +
' }';
const prevStatementNotDevCheckMessage =
'The statement before this coercion must be a DEV check in this format:\n' +
' if (__DEV__) {\n' +
' checkXxxxxStringCoercion(value);\n' +
' }';
function hasCoercionCheck(node) {
let topOfExpression = node;
while (!topOfExpression.parent.body) {
topOfExpression = topOfExpression.parent;
if (!topOfExpression) {
return 'Cannot find top of expression.';
}
}
const containingBlock = topOfExpression.parent.body;
const index = containingBlock.indexOf(topOfExpression);
if (index <= 0) {
return missingDevCheckMessage;
}
const prev = containingBlock[index - 1];
if (
prev.type !== 'IfStatement' ||
prev.test.type !== 'Identifier' ||
prev.test.name !== '__DEV__'
) {
return prevStatementNotDevCheckMessage;
}
let maybeCheckNode = prev.consequent;
if (maybeCheckNode.type === 'BlockStatement') {
const body = maybeCheckNode.body;
if (body.length === 0) {
return prevStatementNotDevCheckMessage;
}
if (body.length !== 1) {
return (
'Too many statements in DEV block before this coercion.' +
' Expected only one (the check function call). ' +
prevStatementNotDevCheckMessage
);
}
maybeCheckNode = body[0];
}
if (maybeCheckNode.type !== 'ExpressionStatement') {
return (
'The DEV block before this coercion must only contain an expression. ' +
prevStatementNotDevCheckMessage
);
}
const call = maybeCheckNode.expression;
if (
call.type !== 'CallExpression' ||
call.callee.type !== 'Identifier' ||
!/^check(\w+?)StringCoercion$/.test(call.callee.name) ||
!call.arguments.length
) {
return (
'Missing or invalid check function call before this coercion.' +
' Expected: call of a function like checkXXXStringCoercion. ' +
prevStatementNotDevCheckMessage
);
}
const same = isEquivalentCode(call.arguments[0], node);
if (!same) {
return (
'Value passed to the check function before this coercion' +
' must match the value being coerced.'
);
}
}
function isOnlyAddingStrings(node) {
if (node.operator !== '+') {
return;
}
if (isStringLiteral(node.left) && isStringLiteral(node.right)) {
return true;
}
if (node.left.type === 'BinaryExpression' && isStringLiteral(node.right)) {
return isOnlyAddingStrings(node.left);
}
}
function checkBinaryExpression(context, node) {
if (isOnlyAddingStrings(node)) {
return;
}
if (
node.operator === '+' &&
(isEmptyLiteral(node.left) || isEmptyLiteral(node.right))
) {
let valueToTest = isEmptyLiteral(node.left) ? node.right : node.left;
if (valueToTest.type === 'TypeCastExpression' && valueToTest.expression) {
valueToTest = valueToTest.expression;
}
if (
valueToTest.type === 'Identifier' &&
['i', 'idx', 'lineNumber'].includes(valueToTest.name)
) {
return;
}
if (
valueToTest.type === 'UnaryExpression' ||
valueToTest.type === 'UpdateExpression'
) {
return;
}
if (isInSafeTypeofBlock(valueToTest)) {
return;
}
const coercionCheckMessage = hasCoercionCheck(valueToTest);
if (!coercionCheckMessage) {
return;
}
context.report({
node,
message:
coercionCheckMessage +
'\n' +
"Using `'' + value` or `value + ''` is fast to coerce strings, but may throw." +
' For prod code, add a DEV check from shared/CheckStringCoercion immediately' +
' before this coercion.' +
' For non-prod code and prod error handling, use `String(value)` instead.',
});
}
}
function coerceWithStringConstructor(context, node) {
const isProductionUserAppCode =
context.options[0] && context.options[0].isProductionUserAppCode;
if (isProductionUserAppCode && node.callee.name === 'String') {
context.report(
node,
"For perf-sensitive coercion, avoid `String(value)`. Instead, use `'' + value`." +
' Precede it with a DEV check from shared/CheckStringCoercion' +
' unless Symbol and Temporal.* values are impossible.' +
' For non-prod code and prod error handling, use `String(value)` and disable this rule.'
);
}
}
module.exports = {
meta: {
schema: [
{
type: 'object',
properties: {
isProductionUserAppCode: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
],
},
create(context) {
return {
BinaryExpression: node => checkBinaryExpression(context, node),
CallExpression: node => coerceWithStringConstructor(context, node),
};
},
};