Skip to content

[New] jsx-no-leaked-render: add ignoreAttributes option #3441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions docs/rules/jsx-no-leaked-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [<enabled>, { "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 (
<MyChildComponent nonChildrenProp={value && 'default'}>
{value && <MyInnerChildComponent />}
</MyChildComponent>
);
}
```

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.
Expand Down
25 changes: 25 additions & 0 deletions lib/rules/jsx-no-leaked-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -137,6 +152,10 @@ module.exports = {
uniqueItems: true,
default: DEFAULT_VALID_STRATEGIES,
},
ignoreAttributes: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
Expand All @@ -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
Expand Down Expand Up @@ -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';
Expand Down
55 changes: 55 additions & 0 deletions tests/lib/rules/jsx-no-leaked-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ ruleTester.run('jsx-no-leaked-render', rule, {
`,
options: [{ validStrategies: ['coerce'] }],
},

// See #3292
{
code: `
const Component = ({ enabled, checked }) => {
return <CheckBox checked={enabled && checked} />
}
`,
options: [{ ignoreAttributes: true }],
},
]) || [],

invalid: parsers.all([].concat(
Expand Down Expand Up @@ -877,6 +887,25 @@ ruleTester.run('jsx-no-leaked-render', rule, {
column: 24,
}],
},

// See #3292
{
code: `
const Component = ({ enabled, checked }) => {
return <CheckBox checked={enabled && checked} />
}
`,
output: `
const Component = ({ enabled, checked }) => {
return <CheckBox checked={enabled ? checked : null} />
}
`,
errors: [{
message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
line: 3,
column: 37,
}],
},
{
code: `
const MyComponent = () => {
Expand Down Expand Up @@ -1002,6 +1031,32 @@ ruleTester.run('jsx-no-leaked-render', rule, {
line: 4,
column: 33,
}],
},
{
code: `
const Component = ({ enabled }) => {
return (
<Foo bar={
<Something>{enabled && <MuchWow />}</Something>
} />
)
}
`,
output: `
const Component = ({ enabled }) => {
return (
<Foo bar={
<Something>{enabled ? <MuchWow /> : null}</Something>
} />
)
}
`,
options: [{ ignoreAttributes: true }],
errors: [{
message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
line: 5,
column: 27,
}],
}
)),
});