From a2ea65a5d0ecf2e2c95b80d99ff54b1ac9e8fdd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 28 Feb 2022 17:48:04 +0100 Subject: [PATCH 1/2] [Refactor] add `isParenthesized` AST util --- CHANGELOG.md | 2 ++ lib/rules/jsx-wrap-multilines.js | 19 +++++-------------- lib/util/ast.js | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c643af0e..8514858751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [Refactor] [`no-deprecated`]: improve performance ([#3271][] @golopot) * [Refactor] [`no-did-mount-set-state`], [`no-did-update-set-state`], [`no-will-update-set-state`]: improve performance ([#3272][] @golopot) * [Refactor] improve performance by avoiding unnecessary `Components.detect` ([#3273][] @golopot) +* [Refactor] add `isParenthesized` AST util ([#3203][] @Belco90) [#3273]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3273 [#3272]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3272 @@ -39,6 +40,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [#3258]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3258 [#3254]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3254 [#3251]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3251 +[#3203]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3203 [#3248]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3248 [#3244]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3244 [#3235]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3235 diff --git a/lib/rules/jsx-wrap-multilines.js b/lib/rules/jsx-wrap-multilines.js index 94600efa61..f865cb970a 100644 --- a/lib/rules/jsx-wrap-multilines.js +++ b/lib/rules/jsx-wrap-multilines.js @@ -9,6 +9,7 @@ const has = require('object.hasown/polyfill')(); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); const reportC = require('../util/report'); +const isParenthesized = require('../util/ast').isParenthesized; // ------------------------------------------------------------------------------ // Constants @@ -89,20 +90,10 @@ module.exports = { return option && option !== 'ignore'; } - function isParenthesised(node) { - const sourceCode = context.getSourceCode(); - const previousToken = sourceCode.getTokenBefore(node); - const nextToken = sourceCode.getTokenAfter(node); - - return previousToken && nextToken - && previousToken.value === '(' && previousToken.range[1] <= node.range[0] - && nextToken.value === ')' && nextToken.range[0] >= node.range[1]; - } - function needsOpeningNewLine(node) { const previousToken = context.getSourceCode().getTokenBefore(node); - if (!isParenthesised(node)) { + if (!isParenthesized(context, node)) { return false; } @@ -116,7 +107,7 @@ module.exports = { function needsClosingNewLine(node) { const nextToken = context.getSourceCode().getTokenAfter(node); - if (!isParenthesised(node)) { + if (!isParenthesized(context, node)) { return false; } @@ -153,12 +144,12 @@ module.exports = { const sourceCode = context.getSourceCode(); const option = getOption(type); - if ((option === true || option === 'parens') && !isParenthesised(node) && isMultilines(node)) { + if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) { report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); } if (option === 'parens-new-line' && isMultilines(node)) { - if (!isParenthesised(node)) { + if (!isParenthesized(context, node)) { const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true }); const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true }); const start = node.loc.start; diff --git a/lib/util/ast.js b/lib/util/ast.js index d25cab4630..596ae9fdaf 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -294,6 +294,23 @@ function getKeyValue(context, node) { return key.type === 'Identifier' ? key.name : key.value; } +/** + * Checks if a node is surrounded by parenthesis. + * + * @param {object} context - Context from the rule + * @param {ASTNode} node - Node to be checked + * @returns {boolean} + */ +function isParenthesized(context, node) { + const sourceCode = context.getSourceCode(); + const previousToken = sourceCode.getTokenBefore(node); + const nextToken = sourceCode.getTokenAfter(node); + + return !!previousToken && !!nextToken + && previousToken.value === '(' && previousToken.range[1] <= node.range[0] + && nextToken.value === ')' && nextToken.range[0] >= node.range[1]; +} + /** * Checks if a node is being assigned a value: props.bar = 'bar' * @param {ASTNode} node The AST node being checked. @@ -410,6 +427,7 @@ module.exports = { getPropertyNameNode, getComponentProperties, getKeyValue, + isParenthesized, isAssignmentLHS, isClass, isFunction, From ef733fd420c821f7159b28f4a36ce5e09375f9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 6 Feb 2022 19:18:00 +0100 Subject: [PATCH 2/2] [New] add `jsx-no-leaked-render` --- CHANGELOG.md | 2 + README.md | 1 + docs/rules/jsx-no-leaked-render.md | 208 ++++++++ index.js | 1 + lib/rules/jsx-no-leaked-render.js | 134 +++++ tests/lib/rules/jsx-no-leaked-render.js | 682 ++++++++++++++++++++++++ 6 files changed, 1028 insertions(+) create mode 100644 docs/rules/jsx-no-leaked-render.md create mode 100644 lib/rules/jsx-no-leaked-render.js create mode 100644 tests/lib/rules/jsx-no-leaked-render.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8514858751..9c04094309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [`no-unknown-property`]: Allow crossOrigin on image tag (SVG) ([#3251][] @zpao) * [`jsx-tag-spacing`]: Add `multiline-always` option ([#3260][] @Nokel81) * [`function-component-definition`]: replace `var` by `const` in certain situations ([#3248][] @JohnBerd @SimeonC) +* add [`jsx-no-leaked-render`] ([#3203][] @Belco90) ### Fixed * [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers) @@ -3685,6 +3686,7 @@ If you're still not using React 15 you can keep the old behavior by setting the [`jsx-no-comment-textnodes`]: docs/rules/jsx-no-comment-textnodes.md [`jsx-no-constructed-context-values`]: docs/rules/jsx-no-constructed-context-values.md [`jsx-no-duplicate-props`]: docs/rules/jsx-no-duplicate-props.md +[`jsx-no-leaked-render`]: docs/rules/jsx-no-leaked-render.md [`jsx-no-literals`]: docs/rules/jsx-no-literals.md [`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md [`jsx-no-target-blank`]: docs/rules/jsx-no-target-blank.md diff --git a/README.md b/README.md index 3bffef410e..775686b85b 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ Enable the rules that you would like to use. | ✔ | | [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) | Comments inside children section of tag should be placed inside braces | | | | [react/jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Prevents JSX context provider values from taking values that will cause needless rerenders. | | ✔ | | [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) | Enforce no duplicate props | +| | 🔧 | [react/jsx-no-leaked-render](docs/rules/jsx-no-leaked-render.md) | Prevent problematic leaked values from being rendered | | | | [react/jsx-no-literals](docs/rules/jsx-no-literals.md) | Prevent using string literals in React component definition | | | | [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md) | Forbid `javascript:` URLs | | ✔ | 🔧 | [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md) | Forbid `target="_blank"` attribute without `rel="noreferrer"` | diff --git a/docs/rules/jsx-no-leaked-render.md b/docs/rules/jsx-no-leaked-render.md new file mode 100644 index 0000000000..597d41890a --- /dev/null +++ b/docs/rules/jsx-no-leaked-render.md @@ -0,0 +1,208 @@ +# Prevent problematic leaked values from being rendered (react/jsx-no-leaked-render) + +Using the `&&` operator to render some element conditionally in JSX can cause unexpected values being rendered, or even crashing the rendering. + + +## Rule Details + +This rule aims to prevent dangerous leaked values from being rendered since they can cause unexpected values reaching the final DOM or even crashing your render method. + +In React, you might end up rendering unexpected values like `0` or `NaN`. In React Native, your render method will crash if you render `0`, `''`, or `NaN`: + +```jsx +const Example = () => { + return ( + <> + {0 && } + {/* React: renders undesired 0 */} + {/* React Native: crashes 💥 */} + + {'' && } + {/* React: renders nothing */} + {/* React Native: crashes 💥 */} + + {NaN && } + {/* React: renders undesired NaN */} + {/* React Native: crashes 💥 */} + + ) +} +``` + +This can be avoided by: +- coercing the conditional to a boolean: `{!!someValue && }` +- transforming the binary expression into a ternary expression which returns `null` for falsy values: `{someValue ? : null}` + +This rule is autofixable; check the Options section to read more about the different strategies available. + +Examples of **incorrect** code for this rule: + +```jsx +const Component = ({ count, title }) => { + return
{count && title}
+} +``` + +```jsx +const Component = ({ count }) => { + return
{count && There are {count} results}
+} +``` + +```jsx +const Component = ({ elements }) => { + return
{elements.length && }
+} +``` + +```jsx +const Component = ({ nestedCollection }) => { + return ( +
+ {nestedCollection.elements.length && } +
+ ) +} +``` + +```jsx +const Component = ({ elements }) => { + return
{elements[0] && }
+} +``` + +```jsx +const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA+numberB}}
+} +``` + +```jsx +// If the condition is a boolean value, this rule will report the logical expression +// since it can't infer the type of the condition. +const Component = ({ someBool }) => { + return
{someBool && {numberA+numberB}}
+} +``` + +Examples of **correct** code for this rule: + +```jsx +const Component = ({ elements }) => { + return
{elements}
+} +``` + +```jsx +// An OR condition it's considered valid since it's assumed as a way +// to render some fallback if the first value is falsy, not to render something conditionally. +const Component = ({ customTitle }) => { + return
{customTitle || defaultTitle}
+} +``` + +```jsx +const Component = ({ elements }) => { + return
There are {elements.length} elements
+} +``` + +```jsx +const Component = ({ elements, count }) => { + return
{!count && 'No results found'}
+} +``` + +```jsx +const Component = ({ elements }) => { + return
{!!elements.length && }
+} +``` + +```jsx +const Component = ({ elements }) => { + return
{Boolean(elements.length) && }
+} +``` + +```jsx +const Component = ({ elements }) => { + return
{elements.length > 0 && }
+} +``` + +```jsx +const Component = ({ elements }) => { + return
{elements.length ? : null}
+} +``` + +### Options + +The supported options are: + +### `validStrategies` +An array containing `"coerce"`, `"ternary"`, or both (default: `["ternary", "coerce"]`) - Decide which strategies are considered valid to prevent leaked renders (at least 1 is required). The "coerce" option will transform the conditional of the JSX expression to a boolean. The "ternary" option transforms the binary expression into a ternary expression returning `null` for falsy values. The first option from the array will be the strategy used when autofixing, so the order of the values matters. + +It can be set like: +```json5 +{ + // ... + "react/jsx-no-leaked-render": [, { "validStrategies": ["ternary", "coerce"] }] + // ... +} +``` + +Assuming the following options: `{ "validStrategies": ["ternary"] }` + +Examples of **incorrect** code for this rule, with the above configuration: +```jsx +const Component = ({ count, title }) => { + return
{count && title}
+} +``` + +```jsx +const Component = ({ count, title }) => { + return
{!!count && title}
+} +``` + +Examples of **correct** code for this rule, with the above configuration: +```jsx +const Component = ({ count, title }) => { + return
{count ? title : null}
+} +``` + +Assuming the following options: `{ "validStrategies": ["coerce"] }` + +Examples of **incorrect** code for this rule, with the above configuration: +```jsx +const Component = ({ count, title }) => { + return
{count && title}
+} +``` + +```jsx +const Component = ({ count, title }) => { + return
{count ? title : null}
+} +``` + +Examples of **correct** code for this rule, with the above configuration: +```jsx +const Component = ({ count, title }) => { + return
{!!count && title}
+} +``` + +## When Not To Use It + +If you are working in a typed-codebase which encourages you to always use boolean conditions, this rule can be disabled. + +## Further Reading + +- [React docs: Inline If with Logical && Operator](https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator) +- [Good advice on JSX conditionals - Beware of zero](https://thoughtspile.github.io/2022/01/17/jsx-conditionals/) +- [Twitter: rendering falsy values in React and React Native](https://twitter.com/kadikraman/status/1507654900376875011?s=21&t=elEXXbHhzWthrgKaPRMjNg) diff --git a/index.js b/index.js index baabf87df4..be3c94a992 100644 --- a/index.js +++ b/index.js @@ -38,6 +38,7 @@ const allRules = { 'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'), 'jsx-no-constructed-context-values': require('./lib/rules/jsx-no-constructed-context-values'), 'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'), + 'jsx-no-leaked-render': require('./lib/rules/jsx-no-leaked-render'), 'jsx-no-literals': require('./lib/rules/jsx-no-literals'), 'jsx-no-script-url': require('./lib/rules/jsx-no-script-url'), 'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'), diff --git a/lib/rules/jsx-no-leaked-render.js b/lib/rules/jsx-no-leaked-render.js new file mode 100644 index 0000000000..8141d25518 --- /dev/null +++ b/lib/rules/jsx-no-leaked-render.js @@ -0,0 +1,134 @@ +/** + * @fileoverview Prevent problematic leaked values from being rendered + * @author Mario Beltrán + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); +const report = require('../util/report'); +const isParenthesized = require('../util/ast').isParenthesized; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const messages = { + noPotentialLeakedRender: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', +}; + +const COERCE_STRATEGY = 'coerce'; +const TERNARY_STRATEGY = 'ternary'; +const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY]; +const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression']; + +function trimLeftNode(node) { + // Remove double unary expression (boolean coercion), so we avoid trimming valid negations + if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') { + return trimLeftNode(node.argument.argument); + } + + return node; +} + +function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) { + const sourceCode = context.getSourceCode(); + const rightSideText = sourceCode.getText(rightNode); + + if (fixStrategy === COERCE_STRATEGY) { + let leftSideText = sourceCode.getText(leftNode); + if (isParenthesized(context, leftNode)) { + leftSideText = `(${leftSideText})`; + } + + const shouldPrefixDoubleNegation = leftNode.type !== 'UnaryExpression'; + + return fixer.replaceText(reportedNode, `${shouldPrefixDoubleNegation ? '!!' : ''}${leftSideText} && ${rightSideText}`); + } + + if (fixStrategy === TERNARY_STRATEGY) { + let leftSideText = sourceCode.getText(trimLeftNode(leftNode)); + if (isParenthesized(context, leftNode)) { + leftSideText = `(${leftSideText})`; + } + return fixer.replaceText(reportedNode, `${leftSideText} ? ${rightSideText} : null`); + } + + throw new TypeError('Invalid value for "validStrategies" option'); +} + +/** + * @type {import('eslint').Rule.RuleModule} + */ +module.exports = { + meta: { + docs: { + description: 'Prevent problematic leaked values from being rendered', + category: 'Possible Errors', + recommended: false, + url: docsUrl('jsx-no-leaked-render'), + }, + + messages, + + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + validStrategies: { + type: 'array', + items: { + enum: [ + TERNARY_STRATEGY, + COERCE_STRATEGY, + ], + }, + uniqueItems: true, + default: DEFAULT_VALID_STRATEGIES, + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const config = context.options[0] || {}; + const validStrategies = new Set(config.validStrategies || DEFAULT_VALID_STRATEGIES); + const fixStrategy = Array.from(validStrategies)[0]; + + return { + 'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) { + const leftSide = node.left; + + if ( + validStrategies.has(COERCE_STRATEGY) + && COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === leftSide.type) + ) { + return; + } + + report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', { + node, + fix(fixer) { + return ruleFixer(context, fixStrategy, fixer, node, leftSide, node.right); + }, + }); + }, + + 'JSXExpressionContainer > ConditionalExpression'(node) { + if (validStrategies.has(TERNARY_STRATEGY)) { + return; + } + + report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', { + node, + fix(fixer) { + return ruleFixer(context, fixStrategy, fixer, node, node.test, node.consequent); + }, + }); + }, + }; + }, +}; diff --git a/tests/lib/rules/jsx-no-leaked-render.js b/tests/lib/rules/jsx-no-leaked-render.js new file mode 100644 index 0000000000..911a859741 --- /dev/null +++ b/tests/lib/rules/jsx-no-leaked-render.js @@ -0,0 +1,682 @@ +/** + * @fileoverview Prevent problematic leaked values from being rendered + * @author Mario Beltrán + */ + +'use strict'; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/jsx-no-leaked-render'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions }); +ruleTester.run('jsx-no-leaked-render', rule, { + valid: parsers.all([ + { + code: ` + const Component = () => { + return
{customTitle || defaultTitle}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
There are {elements.length} elements
+ } + `, + }, + { + code: ` + const Component = ({ elements, count }) => { + return
{!count && 'No results found'}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{!!elements.length && }
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{Boolean(elements.length) && }
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements.length > 0 && }
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements.length ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ elements, count }) => { + return
{count ? : null}
+ } + `, + }, + { + options: [{ validStrategies: ['ternary'] }], + code: ` + const Component = ({ elements, count }) => { + return
{count ? : null}
+ } + `, + }, + { + options: [{ validStrategies: ['coerce'] }], + code: ` + const Component = ({ elements, count }) => { + return
{!!count && }
+ } + `, + }, + { + options: [{ validStrategies: ['coerce', 'ternary'] }], + code: ` + const Component = ({ elements, count }) => { + return
{count ? : null}
+ } + `, + }, + { + options: [{ validStrategies: ['coerce', 'ternary'] }], + code: ` + const Component = ({ elements, count }) => { + return
{!!count && }
+ } + `, + }, + ]), + + invalid: parsers.all([ + // Common invalid cases with default options + { + code: ` + const Example = () => { + return ( + <> + {0 && } + {'' && } + {NaN && } + + ) + } + `, + features: ['fragment'], + errors: [ + { + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 5, + column: 14, + }, + { + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 6, + column: 14, + }, + { + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 7, + column: 14, + }, + ], + output: ` + const Example = () => { + return ( + <> + {0 ? : null} + {'' ? : null} + {NaN ? : null} + + ) + } + `, + }, + + // Invalid tests with both strategies enabled (default) + { + code: ` + const Component = ({ count, title }) => { + return
{count && title}
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{count ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count }) => { + return
{count && There are {count} results}
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count }) => { + return
{count ? There are {count} results : null}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements.length && }
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{elements.length ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ nestedCollection }) => { + return
{nestedCollection.elements.length && }
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ nestedCollection }) => { + return
{nestedCollection.elements.length ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements[0] && }
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{elements[0] ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA+numberB}}
+ } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) ? {numberA+numberB} : null}
+ } + `, + }, + { + code: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA+numberB}}
+ } + `, + options: [{ validStrategies: ['coerce', 'ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ numberA, numberB }) => { + return
{!!(numberA || numberB) && {numberA+numberB}}
+ } + `, + }, + + // Invalid tests only with "ternary" strategy enabled + { + code: ` + const Component = ({ count, title }) => { + return
{count && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{count ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count }) => { + return
{count && There are {count} results}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count }) => { + return
{count ? There are {count} results : null}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements.length && }
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{elements.length ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ nestedCollection }) => { + return
{nestedCollection.elements.length && }
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ nestedCollection }) => { + return
{nestedCollection.elements.length ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements[0] && }
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{elements[0] ? : null}
+ } + `, + }, + { + code: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA+numberB}}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) ? {numberA+numberB} : null}
+ } + `, + }, + + // cases: boolean coerce isn't valid if strategy is only "ternary" + { + code: ` + const Component = ({ someCondition, title }) => { + return
{!someCondition && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ someCondition, title }) => { + return
{!someCondition ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count, title }) => { + return
{!!count && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{count ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count, title }) => { + return
{count > 0 && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{count > 0 ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count, title }) => { + return
{0 != count && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{0 != count ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count, total, title }) => { + return
{count < total && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, total, title }) => { + return
{count < total ? title : null}
+ } + `, + }, + { + code: ` + const Component = ({ count, title, somethingElse }) => { + return
{!!(count && somethingElse) && title}
+ } + `, + options: [{ validStrategies: ['ternary'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title, somethingElse }) => { + return
{count && somethingElse ? title : null}
+ } + `, + }, + + // Invalid tests only with "coerce" strategy enabled + { + code: ` + const Component = ({ count, title }) => { + return
{count && title}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{!!count && title}
+ } + `, + }, + { + code: ` + const Component = ({ count }) => { + return
{count && There are {count} results}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count }) => { + return
{!!count && There are {count} results}
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements.length && }
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{!!elements.length && }
+ } + `, + }, + { + code: ` + const Component = ({ nestedCollection }) => { + return
{nestedCollection.elements.length && }
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ nestedCollection }) => { + return
{!!nestedCollection.elements.length && }
+ } + `, + }, + { + code: ` + const Component = ({ elements }) => { + return
{elements[0] && }
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ elements }) => { + return
{!!elements[0] && }
+ } + `, + }, + { + code: ` + const Component = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA+numberB}}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ numberA, numberB }) => { + return
{!!(numberA || numberB) && {numberA+numberB}}
+ } + `, + }, + + // cases: ternary isn't valid if strategy is only "coerce" + { + code: ` + const Component = ({ count, title }) => { + return
{count ? title : null}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{!!count && title}
+ } + `, + }, + { + code: ` + const Component = ({ count, title }) => { + return
{!count ? title : null}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, title }) => { + return
{!count && title}
+ } + `, + }, + { + code: ` + const Component = ({ count, somethingElse, title }) => { + return
{count && somethingElse ? title : null}
+ } + `, + options: [{ validStrategies: ['coerce'] }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 24, + }], + output: ` + const Component = ({ count, somethingElse, title }) => { + return
{!!count && somethingElse && title}
+ } + `, + }, + ]), +});