diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77c643af0e..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)
@@ -26,6 +27,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 +41,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
@@ -3683,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/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,
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}
+ }
+ `,
+ },
+ ]),
+});