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..8e25d3eb42 --- /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: +- casting the condition to bool: `{!!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 `"cast"`, `"ternary"` or both (default: `["ternary", "cast"]`) - Decide which strategies are considered valid to prevent leaked renders (at least 1 is required). The "cast" option will cast to boolean the condition of the JSX expression. The "ternary" option transforms the binary expression into a ternary expression returning `null` for falsy values. The first option from the array will be used as autofix, so the order of the values matter. + +It can be set like: +```json5 +{ + // ... + "react/jsx-no-leaked-render": [, { "validStrategies": ["ternary", "cast"] }] + // ... +} +``` + +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": ["cast"] }` + +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..5321de3079 --- /dev/null +++ b/lib/rules/jsx-no-leaked-render.js @@ -0,0 +1,137 @@ +/** + * @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 CAST_STRATEGY = 'cast'; +const TERNARY_STRATEGY = 'ternary'; +const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, CAST_STRATEGY]; + +/** + * @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, + CAST_STRATEGY, + ], + }, + uniqueItems: true, + default: DEFAULT_VALID_STRATEGIES, + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const config = context.options[0] || {}; + const validStrategies = config.validStrategies || DEFAULT_VALID_STRATEGIES; + const fixStrategy = validStrategies[0]; + const areBothStrategiesValid = validStrategies.length === 2; + + function trimLeftNode(node) { + // Remove double unary expression (boolean cast), so we avoid trimming valid negations + if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') { + return trimLeftNode(node.argument.argument); + } + + return node; + } + + function ruleFixer(fixer, reportedNode, leftNode, rightNode) { + const sourceCode = context.getSourceCode(); + const rightSideText = sourceCode.getText(rightNode); + + if (fixStrategy === CAST_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 Error('Invalid value for "fixStrategy" option'); + } + + return { + 'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) { + const leftSide = node.left; + const CAST_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression']; + const isCastStrategyValid = areBothStrategiesValid || fixStrategy === CAST_STRATEGY; + const isCastValidLeftExpression = CAST_VALID_LEFT_SIDE_EXPRESSIONS.some( + (validExpression) => validExpression === leftSide.type + ); + + if (isCastStrategyValid && isCastValidLeftExpression) { + return; + } + + report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', { + node, + fix(fixer) { + return ruleFixer(fixer, node, leftSide, node.right); + }, + }); + }, + + 'JSXExpressionContainer > ConditionalExpression'(node) { + const isTernaryStrategyValid = areBothStrategiesValid || fixStrategy === TERNARY_STRATEGY; + if (isTernaryStrategyValid) { + return; + } + + report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', { + node, + fix(fixer) { + return ruleFixer(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..53cdb2d95d --- /dev/null +++ b/tests/lib/rules/jsx-no-leaked-render.js @@ -0,0 +1,681 @@ +/** + * @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: ['cast'] }], + code: ` + const Component = ({ elements, count }) => { + return
{!!count && }
+ } + `, + }, + { + options: [{ validStrategies: ['cast', 'ternary'] }], + code: ` + const Component = ({ elements, count }) => { + return
{count ? : null}
+ } + `, + }, + { + options: [{ validStrategies: ['cast', 'ternary'] }], + code: ` + const Component = ({ elements, count }) => { + return
{!!count && }
+ } + `, + }, + ]), + + invalid: parsers.all([ + // Common invalid cases with default options + { + code: ` + const Example = () => { + return ( + <> + {0 && } + {'' && } + {NaN && } + + ) + } + `, + 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: ['cast', '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 cast 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 "cast" strategy enabled + { + code: ` + const Component = ({ count, title }) => { + return
{count && title}
+ } + `, + options: [{ validStrategies: ['cast'] }], + 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: ['cast'] }], + 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: ['cast'] }], + 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: ['cast'] }], + 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: ['cast'] }], + 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: ['cast'] }], + 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 "cast" + { + code: ` + const Component = ({ count, title }) => { + return
{count ? title : null}
+ } + `, + options: [{ validStrategies: ['cast'] }], + 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: ['cast'] }], + 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: ['cast'] }], + 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}
+ } + `, + }, + ]), +});