Skip to content

Commit ab154a4

Browse files
aleclarsonljharb
authored andcommitted
[New] jsx-no-leaked-render: add ignoreAttributes option
When true, validation of JSX attribute values is skipped.
1 parent 5c23573 commit ab154a4

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

Diff for: docs/rules/jsx-no-leaked-render.md

+30
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ const Component = ({ elements }) => {
151151

152152
The supported options are:
153153

154+
### `ignoreAttributes`
155+
156+
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`.
157+
158+
It can be set like:
159+
160+
```jsonc
161+
{
162+
// ...
163+
"react/jsx-no-leaked-render": [<enabled>, { "ignoreAttributes": true }]
164+
// ...
165+
}
166+
```
167+
168+
Example of incorrect usage with default setting (`ignoreAttributes: false`) and the rule enabled (consider `value` might be undefined):
169+
170+
```jsx
171+
function MyComponent({ value }) {
172+
return (
173+
<MyChildComponent nonChildrenProp={value && 'default'}>
174+
{value && <MyInnerChildComponent />}
175+
</MyChildComponent>
176+
);
177+
}
178+
```
179+
180+
This would trigger a warning in both `nonChildrenProp` and `children` props because `value` might be undefined.
181+
182+
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**.
183+
154184
### `validStrategies`
155185

156186
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 for: lib/rules/jsx-no-leaked-render.js

+25
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ function extractExpressionBetweenLogicalAnds(node) {
5555
);
5656
}
5757

58+
const stopTypes = {
59+
__proto__: null,
60+
JSXElement: true,
61+
JSXFragment: true,
62+
};
63+
64+
function isWithinAttribute(node) {
65+
let parent = node.parent;
66+
while (!stopTypes[parent.type]) {
67+
if (parent.type === 'JSXAttribute') return true;
68+
parent = parent.parent;
69+
}
70+
return false;
71+
}
72+
5873
function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
5974
const rightSideText = getText(context, rightNode);
6075

@@ -137,6 +152,10 @@ module.exports = {
137152
uniqueItems: true,
138153
default: DEFAULT_VALID_STRATEGIES,
139154
},
155+
ignoreAttributes: {
156+
type: 'boolean',
157+
default: false,
158+
},
140159
},
141160
additionalProperties: false,
142161
},
@@ -150,6 +169,9 @@ module.exports = {
150169

151170
return {
152171
'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
172+
if (config.ignoreAttributes && isWithinAttribute(node)) {
173+
return;
174+
}
153175
const leftSide = node.left;
154176

155177
const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
@@ -185,6 +207,9 @@ module.exports = {
185207
if (validStrategies.has(TERNARY_STRATEGY)) {
186208
return;
187209
}
210+
if (config.ignoreAttributes && isWithinAttribute(node)) {
211+
return;
212+
}
188213

189214
const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
190215
const isJSXElementAlternate = node.alternate.type === 'JSXElement';

Diff for: tests/lib/rules/jsx-no-leaked-render.js

+55
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,16 @@ ruleTester.run('jsx-no-leaked-render', rule, {
205205
`,
206206
options: [{ validStrategies: ['coerce'] }],
207207
},
208+
209+
// See #3292
210+
{
211+
code: `
212+
const Component = ({ enabled, checked }) => {
213+
return <CheckBox checked={enabled && checked} />
214+
}
215+
`,
216+
options: [{ ignoreAttributes: true }],
217+
},
208218
]) || [],
209219

210220
invalid: parsers.all([].concat(
@@ -877,6 +887,25 @@ ruleTester.run('jsx-no-leaked-render', rule, {
877887
column: 24,
878888
}],
879889
},
890+
891+
// See #3292
892+
{
893+
code: `
894+
const Component = ({ enabled, checked }) => {
895+
return <CheckBox checked={enabled && checked} />
896+
}
897+
`,
898+
output: `
899+
const Component = ({ enabled, checked }) => {
900+
return <CheckBox checked={enabled ? checked : null} />
901+
}
902+
`,
903+
errors: [{
904+
message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
905+
line: 3,
906+
column: 37,
907+
}],
908+
},
880909
{
881910
code: `
882911
const MyComponent = () => {
@@ -1002,6 +1031,32 @@ ruleTester.run('jsx-no-leaked-render', rule, {
10021031
line: 4,
10031032
column: 33,
10041033
}],
1034+
},
1035+
{
1036+
code: `
1037+
const Component = ({ enabled }) => {
1038+
return (
1039+
<Foo bar={
1040+
<Something>{enabled && <MuchWow />}</Something>
1041+
} />
1042+
)
1043+
}
1044+
`,
1045+
output: `
1046+
const Component = ({ enabled }) => {
1047+
return (
1048+
<Foo bar={
1049+
<Something>{enabled ? <MuchWow /> : null}</Something>
1050+
} />
1051+
)
1052+
}
1053+
`,
1054+
options: [{ ignoreAttributes: true }],
1055+
errors: [{
1056+
message: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
1057+
line: 5,
1058+
column: 27,
1059+
}],
10051060
}
10061061
)),
10071062
});

0 commit comments

Comments
 (0)