From ef7476289cf6480773ac462792584f123a4cd158 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 26 Sep 2022 12:50:50 -0400 Subject: [PATCH] [New] `jsx-no-leaked-render`: add `ignoreAttributes` option When true, validation of JSX attribute values is skipped. --- CHANGELOG.md | 5 +++ docs/rules/jsx-no-leaked-render.md | 30 ++++++++++++++ lib/rules/jsx-no-leaked-render.js | 25 +++++++++++ tests/lib/rules/jsx-no-leaked-render.js | 55 +++++++++++++++++++++++++ 4 files changed, 115 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26389bca1e..136bac74e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Added +* [`jsx-no-leaked-render`]: add `ignoreAttributes` option ([#3441][] @aleclarson) + +[#3441]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3441 + ## [7.37.2] - 2024.10.22 ### Fixed diff --git a/docs/rules/jsx-no-leaked-render.md b/docs/rules/jsx-no-leaked-render.md index 88b74838ab..e978faebde 100644 --- a/docs/rules/jsx-no-leaked-render.md +++ b/docs/rules/jsx-no-leaked-render.md @@ -151,6 +151,36 @@ const Component = ({ elements }) => { The supported options are: +### `ignoreAttributes` + +Boolean. When set to `true`, this option ignores all attributes except for `children` during validation, preventing false positives in scenarios where these attributes are used safely or validated internally. Default is `false`. + +It can be set like: + +```jsonc +{ + // ... + "react/jsx-no-leaked-render": [, { "ignoreAttributes": true }] + // ... +} +``` + +Example of incorrect usage with default setting (`ignoreAttributes: false`) and the rule enabled (consider `value` might be undefined): + +```jsx +function MyComponent({ value }) { + return ( + + {value && } + + ); +} +``` + +This would trigger a warning in both `nonChildrenProp` and `children` props because `value` might be undefined. + +By setting `ignoreAttributes` to `true`, the rule will not flag this scenario in `nonChildrenProp`, reducing false positives, **but will keep the warning of `children` being leaked**. + ### `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. diff --git a/lib/rules/jsx-no-leaked-render.js b/lib/rules/jsx-no-leaked-render.js index 1e271b2a68..efe7018c21 100644 --- a/lib/rules/jsx-no-leaked-render.js +++ b/lib/rules/jsx-no-leaked-render.js @@ -55,6 +55,21 @@ function extractExpressionBetweenLogicalAnds(node) { ); } +const stopTypes = { + __proto__: null, + JSXElement: true, + JSXFragment: true, +}; + +function isWithinAttribute(node) { + let parent = node.parent; + while (!stopTypes[parent.type]) { + if (parent.type === 'JSXAttribute') return true; + parent = parent.parent; + } + return false; +} + function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) { const rightSideText = getText(context, rightNode); @@ -137,6 +152,10 @@ module.exports = { uniqueItems: true, default: DEFAULT_VALID_STRATEGIES, }, + ignoreAttributes: { + type: 'boolean', + default: false, + }, }, additionalProperties: false, }, @@ -150,6 +169,9 @@ module.exports = { return { 'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) { + if (config.ignoreAttributes && isWithinAttribute(node)) { + return; + } const leftSide = node.left; const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS @@ -185,6 +207,9 @@ module.exports = { if (validStrategies.has(TERNARY_STRATEGY)) { return; } + if (config.ignoreAttributes && isWithinAttribute(node)) { + return; + } const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1; const isJSXElementAlternate = node.alternate.type === 'JSXElement'; diff --git a/tests/lib/rules/jsx-no-leaked-render.js b/tests/lib/rules/jsx-no-leaked-render.js index 071ecd68ab..aaedb60b7e 100644 --- a/tests/lib/rules/jsx-no-leaked-render.js +++ b/tests/lib/rules/jsx-no-leaked-render.js @@ -205,6 +205,16 @@ ruleTester.run('jsx-no-leaked-render', rule, { `, options: [{ validStrategies: ['coerce'] }], }, + + // See #3292 + { + code: ` + const Component = ({ enabled, checked }) => { + return + } + `, + options: [{ ignoreAttributes: true }], + }, ]) || [], invalid: parsers.all([].concat( @@ -877,6 +887,25 @@ ruleTester.run('jsx-no-leaked-render', rule, { column: 24, }], }, + + // See #3292 + { + code: ` + const Component = ({ enabled, checked }) => { + return + } + `, + output: ` + const Component = ({ enabled, checked }) => { + return + } + `, + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 3, + column: 37, + }], + }, { code: ` const MyComponent = () => { @@ -1002,6 +1031,32 @@ ruleTester.run('jsx-no-leaked-render', rule, { line: 4, column: 33, }], + }, + { + code: ` + const Component = ({ enabled }) => { + return ( + {enabled && } + } /> + ) + } + `, + output: ` + const Component = ({ enabled }) => { + return ( + {enabled ? : null} + } /> + ) + } + `, + options: [{ ignoreAttributes: true }], + errors: [{ + message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes', + line: 5, + column: 27, + }], } )), });