Skip to content

Commit

Permalink
[New] jsx-no-leaked-render: add ignoreAttributes option
Browse files Browse the repository at this point in the history
When true, validation of JSX attribute values is skipped.
  • Loading branch information
aleclarson authored and ljharb committed Sep 26, 2022
1 parent 256cf74 commit ef74762
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 0 deletions.
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,
}],
}
)),
});

0 comments on commit ef74762

Please sign in to comment.