jsx-no-leaked-render.js 6.4 KB

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