jsx-no-leaked-render.js 7.4 KB

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