1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. function isEmptyLiteral(node) {
    
  13.   return (
    
  14.     node.type === 'Literal' &&
    
  15.     typeof node.value === 'string' &&
    
  16.     node.value === ''
    
  17.   );
    
  18. }
    
  19. 
    
  20. function isStringLiteral(node) {
    
  21.   return (
    
  22.     // TaggedTemplateExpressions can return non-strings
    
  23.     (node.type === 'TemplateLiteral' &&
    
  24.       node.parent.type !== 'TaggedTemplateExpression') ||
    
  25.     (node.type === 'Literal' && typeof node.value === 'string')
    
  26.   );
    
  27. }
    
  28. 
    
  29. // Symbols and Temporal.* objects will throw when using `'' + value`, but that
    
  30. // pattern can be faster than `String(value)` because JS engines can optimize
    
  31. // `+` better in some cases. Therefore, in perf-sensitive production codepaths
    
  32. // we require using `'' + value` for string coercion. The only exception is prod
    
  33. // error handling code, because it's bad to crash while assembling an error
    
  34. // message or call stack! Also, error-handling code isn't usually perf-critical.
    
  35. //
    
  36. // Non-production codepaths (tests, devtools extension, build tools, etc.)
    
  37. // should use `String(value)` because it will never crash and the (small) perf
    
  38. // difference doesn't matter enough for non-prod use cases.
    
  39. //
    
  40. // This rule assists enforcing these guidelines:
    
  41. // * `'' + value` is flagged with a message to remind developers to add a DEV
    
  42. //   check from shared/CheckStringCoercion.js to make sure that the user gets a
    
  43. //   clear error message in DEV is the coercion will throw. These checks are not
    
  44. //   needed if throwing is not possible, e.g. if the value is already known to
    
  45. //   be a string or number.
    
  46. // * `String(value)` is flagged only if the `isProductionUserAppCode` option
    
  47. //   is set. Set this option for prod code files, and don't set it for non-prod
    
  48. //   files.
    
  49. 
    
  50. const ignoreKeys = [
    
  51.   'range',
    
  52.   'raw',
    
  53.   'parent',
    
  54.   'loc',
    
  55.   'start',
    
  56.   'end',
    
  57.   '_babelType',
    
  58.   'leadingComments',
    
  59.   'trailingComments',
    
  60. ];
    
  61. function astReplacer(key, value) {
    
  62.   return ignoreKeys.includes(key) ? undefined : value;
    
  63. }
    
  64. 
    
  65. /**
    
  66.  * Simplistic comparison between AST node. Only the following patterns are
    
  67.  * supported because that's almost all (all?) usage in React:
    
  68.  * - Identifiers, e.g. `foo`
    
  69.  * - Member access, e.g. `foo.bar`
    
  70.  * - Array access with numeric literal, e.g. `foo[0]`
    
  71.  */
    
  72. function isEquivalentCode(node1, node2) {
    
  73.   return (
    
  74.     JSON.stringify(node1, astReplacer) === JSON.stringify(node2, astReplacer)
    
  75.   );
    
  76. }
    
  77. 
    
  78. function isDescendant(node, maybeParentNode) {
    
  79.   let parent = node.parent;
    
  80.   while (parent) {
    
  81.     if (!parent) {
    
  82.       return false;
    
  83.     }
    
  84.     if (parent === maybeParentNode) {
    
  85.       return true;
    
  86.     }
    
  87.     parent = parent.parent;
    
  88.   }
    
  89.   return false;
    
  90. }
    
  91. 
    
  92. function isSafeTypeofExpression(originalValueNode, node) {
    
  93.   if (node.type === 'BinaryExpression') {
    
  94.     // Example: typeof foo === 'string'
    
  95.     if (node.operator !== '===') {
    
  96.       return false;
    
  97.     }
    
  98.     const {left, right} = node;
    
  99. 
    
  100.     // left must be `typeof original`
    
  101.     if (left.type !== 'UnaryExpression' || left.operator !== 'typeof') {
    
  102.       return false;
    
  103.     }
    
  104.     if (!isEquivalentCode(left.argument, originalValueNode)) {
    
  105.       return false;
    
  106.     }
    
  107.     // right must be a literal value of a safe type
    
  108.     const safeTypes = ['string', 'number', 'boolean', 'undefined', 'bigint'];
    
  109.     if (right.type !== 'Literal' || !safeTypes.includes(right.value)) {
    
  110.       return false;
    
  111.     }
    
  112.     return true;
    
  113.   } else if (node.type === 'LogicalExpression') {
    
  114.     // Examples:
    
  115.     // * typeof foo === 'string' && typeof foo === 'number
    
  116.     // * typeof foo === 'string' && someOtherTest
    
  117.     if (node.operator === '&&') {
    
  118.       return (
    
  119.         isSafeTypeofExpression(originalValueNode, node.left) ||
    
  120.         isSafeTypeofExpression(originalValueNode, node.right)
    
  121.       );
    
  122.     } else if (node.operator === '||') {
    
  123.       return (
    
  124.         isSafeTypeofExpression(originalValueNode, node.left) &&
    
  125.         isSafeTypeofExpression(originalValueNode, node.right)
    
  126.       );
    
  127.     }
    
  128.   }
    
  129.   return false;
    
  130. }
    
  131. 
    
  132. /**
    
  133.   Returns true if the code is inside an `if` block that validates the value
    
  134.   excludes symbols and objects. Examples:
    
  135.   * if (typeof value === 'string') { }
    
  136.   * if (typeof value === 'string' || typeof value === 'number') { }
    
  137.   * if (typeof value === 'string' || someOtherTest) { }
    
  138. 
    
  139.   @param - originalValueNode Top-level expression to test. Kept unchanged during
    
  140.   recursion.
    
  141.   @param - node Expression to test at current recursion level. Will be undefined
    
  142.   on non-recursive call.
    
  143. */
    
  144. function isInSafeTypeofBlock(originalValueNode, node) {
    
  145.   if (!node) {
    
  146.     node = originalValueNode;
    
  147.   }
    
  148.   let parent = node.parent;
    
  149.   while (parent) {
    
  150.     if (!parent) {
    
  151.       return false;
    
  152.     }
    
  153.     // Normally, if the parent block is inside a type-safe `if` statement,
    
  154.     // then all child code is also type-safe. But there's a quirky case we
    
  155.     // need to defend against:
    
  156.     //   if (typeof obj === 'string') { } else if (typeof obj === 'object') {'' + obj}
    
  157.     //   if (typeof obj === 'string') { } else {'' + obj}
    
  158.     // In that code above, the `if` block is safe, but the `else` block is
    
  159.     // unsafe and should report. But the AST parent of the `else` clause is the
    
  160.     // `if` statement. This is the one case where the parent doesn't confer
    
  161.     // safety onto the child. The code below identifies that case and keeps
    
  162.     // moving up the tree until we get out of the `else`'s parent `if` block.
    
  163.     // This ensures that we don't use any of these "parents" (really siblings)
    
  164.     // to confer safety onto the current node.
    
  165.     if (
    
  166.       parent.type === 'IfStatement' &&
    
  167.       !isDescendant(originalValueNode, parent.alternate)
    
  168.     ) {
    
  169.       const test = parent.test;
    
  170.       if (isSafeTypeofExpression(originalValueNode, test)) {
    
  171.         return true;
    
  172.       }
    
  173.     }
    
  174.     parent = parent.parent;
    
  175.   }
    
  176. }
    
  177. 
    
  178. const missingDevCheckMessage =
    
  179.   'Missing DEV check before this string coercion.' +
    
  180.   ' Check should be in this format:\n' +
    
  181.   '  if (__DEV__) {\n' +
    
  182.   '    checkXxxxxStringCoercion(value);\n' +
    
  183.   '  }';
    
  184. 
    
  185. const prevStatementNotDevCheckMessage =
    
  186.   'The statement before this coercion must be a DEV check in this format:\n' +
    
  187.   '  if (__DEV__) {\n' +
    
  188.   '    checkXxxxxStringCoercion(value);\n' +
    
  189.   '  }';
    
  190. 
    
  191. /**
    
  192.  * Does this node have an "is coercion safe?" DEV check
    
  193.  * in the same block?
    
  194.  */
    
  195. function hasCoercionCheck(node) {
    
  196.   // find the containing statement
    
  197.   let topOfExpression = node;
    
  198.   while (!topOfExpression.parent.body) {
    
  199.     topOfExpression = topOfExpression.parent;
    
  200.     if (!topOfExpression) {
    
  201.       return 'Cannot find top of expression.';
    
  202.     }
    
  203.   }
    
  204.   const containingBlock = topOfExpression.parent.body;
    
  205.   const index = containingBlock.indexOf(topOfExpression);
    
  206.   if (index <= 0) {
    
  207.     return missingDevCheckMessage;
    
  208.   }
    
  209.   const prev = containingBlock[index - 1];
    
  210. 
    
  211.   // The previous statement is expected to be like this:
    
  212.   //   if (__DEV__) {
    
  213.   //     checkFormFieldValueStringCoercion(foo);
    
  214.   //   }
    
  215.   // where `foo` must be equivalent to `node` (which is the
    
  216.   // mixed value being coerced to a string).
    
  217.   if (
    
  218.     prev.type !== 'IfStatement' ||
    
  219.     prev.test.type !== 'Identifier' ||
    
  220.     prev.test.name !== '__DEV__'
    
  221.   ) {
    
  222.     return prevStatementNotDevCheckMessage;
    
  223.   }
    
  224.   let maybeCheckNode = prev.consequent;
    
  225.   if (maybeCheckNode.type === 'BlockStatement') {
    
  226.     const body = maybeCheckNode.body;
    
  227.     if (body.length === 0) {
    
  228.       return prevStatementNotDevCheckMessage;
    
  229.     }
    
  230.     if (body.length !== 1) {
    
  231.       return (
    
  232.         'Too many statements in DEV block before this coercion.' +
    
  233.         ' Expected only one (the check function call). ' +
    
  234.         prevStatementNotDevCheckMessage
    
  235.       );
    
  236.     }
    
  237.     maybeCheckNode = body[0];
    
  238.   }
    
  239. 
    
  240.   if (maybeCheckNode.type !== 'ExpressionStatement') {
    
  241.     return (
    
  242.       'The DEV block before this coercion must only contain an expression. ' +
    
  243.       prevStatementNotDevCheckMessage
    
  244.     );
    
  245.   }
    
  246. 
    
  247.   const call = maybeCheckNode.expression;
    
  248.   if (
    
  249.     call.type !== 'CallExpression' ||
    
  250.     call.callee.type !== 'Identifier' ||
    
  251.     !/^check(\w+?)StringCoercion$/.test(call.callee.name) ||
    
  252.     !call.arguments.length
    
  253.   ) {
    
  254.     // `maybeCheckNode` should be a call of a function named checkXXXStringCoercion
    
  255.     return (
    
  256.       'Missing or invalid check function call before this coercion.' +
    
  257.       ' Expected: call of a function like checkXXXStringCoercion. ' +
    
  258.       prevStatementNotDevCheckMessage
    
  259.     );
    
  260.   }
    
  261. 
    
  262.   const same = isEquivalentCode(call.arguments[0], node);
    
  263.   if (!same) {
    
  264.     return (
    
  265.       'Value passed to the check function before this coercion' +
    
  266.       ' must match the value being coerced.'
    
  267.     );
    
  268.   }
    
  269. }
    
  270. 
    
  271. function isOnlyAddingStrings(node) {
    
  272.   if (node.operator !== '+') {
    
  273.     return;
    
  274.   }
    
  275.   if (isStringLiteral(node.left) && isStringLiteral(node.right)) {
    
  276.     // It's always safe to add string literals
    
  277.     return true;
    
  278.   }
    
  279.   if (node.left.type === 'BinaryExpression' && isStringLiteral(node.right)) {
    
  280.     return isOnlyAddingStrings(node.left);
    
  281.   }
    
  282. }
    
  283. 
    
  284. function checkBinaryExpression(context, node) {
    
  285.   if (isOnlyAddingStrings(node)) {
    
  286.     return;
    
  287.   }
    
  288. 
    
  289.   if (
    
  290.     node.operator === '+' &&
    
  291.     (isEmptyLiteral(node.left) || isEmptyLiteral(node.right))
    
  292.   ) {
    
  293.     let valueToTest = isEmptyLiteral(node.left) ? node.right : node.left;
    
  294.     if (
    
  295.       (valueToTest.type === 'TypeCastExpression' ||
    
  296.         valueToTest.type === 'AsExpression') &&
    
  297.       valueToTest.expression
    
  298.     ) {
    
  299.       valueToTest = valueToTest.expression;
    
  300.     }
    
  301. 
    
  302.     if (
    
  303.       valueToTest.type === 'Identifier' &&
    
  304.       ['i', 'idx', 'lineNumber'].includes(valueToTest.name)
    
  305.     ) {
    
  306.       // Common non-object variable names are assumed to be safe
    
  307.       return;
    
  308.     }
    
  309.     if (
    
  310.       valueToTest.type === 'UnaryExpression' ||
    
  311.       valueToTest.type === 'UpdateExpression'
    
  312.     ) {
    
  313.       // Any unary expression will return a non-object, non-symbol type.
    
  314.       return;
    
  315.     }
    
  316.     if (isInSafeTypeofBlock(valueToTest)) {
    
  317.       // The value is inside an if (typeof...) block that ensures it's safe
    
  318.       return;
    
  319.     }
    
  320.     const coercionCheckMessage = hasCoercionCheck(valueToTest);
    
  321.     if (!coercionCheckMessage) {
    
  322.       // The previous statement is a correct check function call, so no report.
    
  323.       return;
    
  324.     }
    
  325. 
    
  326.     context.report({
    
  327.       node,
    
  328.       message:
    
  329.         coercionCheckMessage +
    
  330.         '\n' +
    
  331.         "Using `'' + value` or `value + ''` is fast to coerce strings, but may throw." +
    
  332.         ' For prod code, add a DEV check from shared/CheckStringCoercion immediately' +
    
  333.         ' before this coercion.' +
    
  334.         ' For non-prod code and prod error handling, use `String(value)` instead.',
    
  335.     });
    
  336.   }
    
  337. }
    
  338. 
    
  339. function coerceWithStringConstructor(context, node) {
    
  340.   const isProductionUserAppCode =
    
  341.     context.options[0] && context.options[0].isProductionUserAppCode;
    
  342.   if (isProductionUserAppCode && node.callee.name === 'String') {
    
  343.     context.report(
    
  344.       node,
    
  345.       "For perf-sensitive coercion, avoid `String(value)`. Instead, use `'' + value`." +
    
  346.         ' Precede it with a DEV check from shared/CheckStringCoercion' +
    
  347.         ' unless Symbol and Temporal.* values are impossible.' +
    
  348.         ' For non-prod code and prod error handling, use `String(value)` and disable this rule.'
    
  349.     );
    
  350.   }
    
  351. }
    
  352. 
    
  353. module.exports = {
    
  354.   meta: {
    
  355.     schema: [
    
  356.       {
    
  357.         type: 'object',
    
  358.         properties: {
    
  359.           isProductionUserAppCode: {
    
  360.             type: 'boolean',
    
  361.             default: false,
    
  362.           },
    
  363.         },
    
  364.         additionalProperties: false,
    
  365.       },
    
  366.     ],
    
  367.   },
    
  368.   create(context) {
    
  369.     return {
    
  370.       BinaryExpression: node => checkBinaryExpression(context, node),
    
  371.       CallExpression: node => coerceWithStringConstructor(context, node),
    
  372.     };
    
  373.   },
    
  374. };