jsx-no-leaked-render.js 5.1 KB

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