jsx-no-leaked-render.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * @fileoverview Prevent problematic leaked values from being rendered
  3. * @author Mario Beltrán
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const report = require('../util/report');
  8. const testReactVersion = require('../util/version').testReactVersion;
  9. const isParenthesized = require('../util/ast').isParenthesized;
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. const messages = {
  14. noPotentialLeakedRender: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
  15. };
  16. const COERCE_STRATEGY = 'coerce';
  17. const TERNARY_STRATEGY = 'ternary';
  18. const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY];
  19. const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression'];
  20. const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false];
  21. function trimLeftNode(node) {
  22. // Remove double unary expression (boolean coercion), so we avoid trimming valid negations
  23. if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') {
  24. return trimLeftNode(node.argument.argument);
  25. }
  26. return node;
  27. }
  28. function getIsCoerceValidNestedLogicalExpression(node) {
  29. if (node.type === 'LogicalExpression') {
  30. return getIsCoerceValidNestedLogicalExpression(node.left) && getIsCoerceValidNestedLogicalExpression(node.right);
  31. }
  32. return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === node.type);
  33. }
  34. function extractExpressionBetweenLogicalAnds(node) {
  35. if (node.type !== 'LogicalExpression') return [node];
  36. if (node.operator !== '&&') return [node];
  37. return [].concat(
  38. extractExpressionBetweenLogicalAnds(node.left),
  39. extractExpressionBetweenLogicalAnds(node.right)
  40. );
  41. }
  42. function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
  43. const sourceCode = context.getSourceCode();
  44. const rightSideText = sourceCode.getText(rightNode);
  45. if (fixStrategy === COERCE_STRATEGY) {
  46. const expressions = extractExpressionBetweenLogicalAnds(leftNode);
  47. const newText = expressions.map((node) => {
  48. let nodeText = sourceCode.getText(node);
  49. if (isParenthesized(context, node)) {
  50. nodeText = `(${nodeText})`;
  51. }
  52. if (node.parent && node.parent.type === 'ConditionalExpression' && node.parent.consequent.value === false) {
  53. return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'}${nodeText}`;
  54. }
  55. return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'}${nodeText}`;
  56. }).join(' && ');
  57. if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && rightNode.parent.consequent.value === false) {
  58. const consequentVal = rightNode.parent.consequent.raw || rightNode.parent.consequent.name;
  59. const alternateVal = rightNode.parent.alternate.raw || rightNode.parent.alternate.name;
  60. if (rightNode.parent.test && rightNode.parent.test.type === 'LogicalExpression') {
  61. return fixer.replaceText(reportedNode, `${newText} ? ${consequentVal} : ${alternateVal}`);
  62. }
  63. return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`);
  64. }
  65. if (rightNode.type === 'ConditionalExpression') {
  66. return fixer.replaceText(reportedNode, `${newText} && (${rightSideText})`);
  67. }
  68. if (rightNode.type === 'Literal') {
  69. return null;
  70. }
  71. return fixer.replaceText(reportedNode, `${newText} && ${rightSideText}`);
  72. }
  73. if (fixStrategy === TERNARY_STRATEGY) {
  74. let leftSideText = sourceCode.getText(trimLeftNode(leftNode));
  75. if (isParenthesized(context, leftNode)) {
  76. leftSideText = `(${leftSideText})`;
  77. }
  78. return fixer.replaceText(reportedNode, `${leftSideText} ? ${rightSideText} : null`);
  79. }
  80. throw new TypeError('Invalid value for "validStrategies" option');
  81. }
  82. /**
  83. * @type {import('eslint').Rule.RuleModule}
  84. */
  85. module.exports = {
  86. meta: {
  87. docs: {
  88. description: 'Disallow problematic leaked values from being rendered',
  89. category: 'Possible Errors',
  90. recommended: false,
  91. url: docsUrl('jsx-no-leaked-render'),
  92. },
  93. messages,
  94. fixable: 'code',
  95. schema: [
  96. {
  97. type: 'object',
  98. properties: {
  99. validStrategies: {
  100. type: 'array',
  101. items: {
  102. enum: [
  103. TERNARY_STRATEGY,
  104. COERCE_STRATEGY,
  105. ],
  106. },
  107. uniqueItems: true,
  108. default: DEFAULT_VALID_STRATEGIES,
  109. },
  110. },
  111. additionalProperties: false,
  112. },
  113. ],
  114. },
  115. create(context) {
  116. const config = context.options[0] || {};
  117. const validStrategies = new Set(config.validStrategies || DEFAULT_VALID_STRATEGIES);
  118. const fixStrategy = Array.from(validStrategies)[0];
  119. return {
  120. 'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
  121. const leftSide = node.left;
  122. const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
  123. .some((validExpression) => validExpression === leftSide.type);
  124. if (validStrategies.has(COERCE_STRATEGY)) {
  125. if (isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression(leftSide)) {
  126. return;
  127. }
  128. }
  129. if (testReactVersion(context, '>= 18') && leftSide.type === 'Literal' && leftSide.value === '') {
  130. return;
  131. }
  132. report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
  133. node,
  134. fix(fixer) {
  135. return ruleFixer(context, fixStrategy, fixer, node, leftSide, node.right);
  136. },
  137. });
  138. },
  139. 'JSXExpressionContainer > ConditionalExpression'(node) {
  140. if (validStrategies.has(TERNARY_STRATEGY)) {
  141. return;
  142. }
  143. const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
  144. const isJSXElementAlternate = node.alternate.type === 'JSXElement';
  145. if (isValidTernaryAlternate || isJSXElementAlternate) {
  146. return;
  147. }
  148. report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
  149. node,
  150. fix(fixer) {
  151. return ruleFixer(context, fixStrategy, fixer, node, node.test, node.consequent);
  152. },
  153. });
  154. },
  155. };
  156. },
  157. };