Skip to content

Commit e7fc22f

Browse files
Belco90ljharb
authored andcommitted
[Fix] jsx-no-leaked-render: avoid unnecessary negation operators and ternary branches deletion
Closes #3297. Relates to #3292.
1 parent c42b624 commit e7fc22f

File tree

3 files changed

+76
-8
lines changed

3 files changed

+76
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
77

88
### Fixed
99
* [`display-name`]: fix false positive for HOF returning only nulls ([#3291][] @golopot)
10+
* [`jsx-no-leaked-render`]: avoid unnecessary negation operators and ternary branches deletion ([#3299][] @Belco90)
1011

1112
### Changed
1213
* [Docs] [`jsx-tag-spacing`]: rename option from [#3264][] ([#3294[] @ljharb)
1314
* [Docs] [`jsx-key`]: split the examples ([#3293][] @ioggstream)
1415

16+
[#3299]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3299
1517
[#3294]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3294
1618
[#3293]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3293
1719
[#3291]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3291

lib/rules/jsx-no-leaked-render.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const COERCE_STRATEGY = 'coerce';
2121
const TERNARY_STRATEGY = 'ternary';
2222
const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY];
2323
const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression'];
24+
const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false];
2425

2526
function trimLeftNode(node) {
2627
// Remove double unary expression (boolean coercion), so we avoid trimming valid negations
@@ -31,6 +32,14 @@ function trimLeftNode(node) {
3132
return node;
3233
}
3334

35+
function getIsCoerceValidNestedLogicalExpression(node) {
36+
if (node.type === 'LogicalExpression') {
37+
return getIsCoerceValidNestedLogicalExpression(node.left) && getIsCoerceValidNestedLogicalExpression(node.right);
38+
}
39+
40+
return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === node.type);
41+
}
42+
3443
function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
3544
const sourceCode = context.getSourceCode();
3645
const rightSideText = sourceCode.getText(rightNode);
@@ -102,11 +111,12 @@ module.exports = {
102111
'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
103112
const leftSide = node.left;
104113

105-
if (
106-
validStrategies.has(COERCE_STRATEGY)
107-
&& COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === leftSide.type)
108-
) {
109-
return;
114+
const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
115+
.some((validExpression) => validExpression === leftSide.type);
116+
if (validStrategies.has(COERCE_STRATEGY)) {
117+
if (isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression(leftSide)) {
118+
return;
119+
}
110120
}
111121

112122
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
@@ -122,6 +132,11 @@ module.exports = {
122132
return;
123133
}
124134

135+
const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
136+
if (isValidTernaryAlternate) {
137+
return;
138+
}
139+
125140
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
126141
node,
127142
fix(fixer) {

tests/lib/rules/jsx-no-leaked-render.js

+54-3
Original file line numberDiff line numberDiff line change
@@ -93,36 +93,87 @@ ruleTester.run('jsx-no-leaked-render', rule, {
9393
`,
9494
},
9595
{
96-
options: [{ validStrategies: ['ternary'] }],
9796
code: `
9897
const Component = ({ elements, count }) => {
9998
return <div>{count ? <List elements={elements}/> : null}</div>
10099
}
101100
`,
101+
options: [{ validStrategies: ['ternary'] }],
102102
},
103103
{
104-
options: [{ validStrategies: ['coerce'] }],
105104
code: `
106105
const Component = ({ elements, count }) => {
107106
return <div>{!!count && <List elements={elements}/>}</div>
108107
}
109108
`,
109+
options: [{ validStrategies: ['coerce'] }],
110110
},
111111
{
112-
options: [{ validStrategies: ['coerce', 'ternary'] }],
113112
code: `
114113
const Component = ({ elements, count }) => {
115114
return <div>{count ? <List elements={elements}/> : null}</div>
116115
}
117116
`,
117+
options: [{ validStrategies: ['coerce', 'ternary'] }],
118118
},
119119
{
120+
code: `
121+
const Component = ({ elements, count }) => {
122+
return <div>{!!count && <List elements={elements}/>}</div>
123+
}
124+
`,
120125
options: [{ validStrategies: ['coerce', 'ternary'] }],
126+
},
127+
{
121128
code: `
122129
const Component = ({ elements, count }) => {
123130
return <div>{!!count && <List elements={elements}/>}</div>
124131
}
125132
`,
133+
options: [{ validStrategies: ['coerce'] }],
134+
},
135+
136+
// Fixes for:
137+
// - https://github.com/jsx-eslint/eslint-plugin-react/issues/3292
138+
// - https://github.com/jsx-eslint/eslint-plugin-react/issues/3297
139+
{
140+
// It shouldn't delete valid alternate from ternary expressions when "coerce" is the only valid strategy
141+
code: `
142+
const Component = ({ elements, count }) => {
143+
return (
144+
<div>
145+
<div> {direction ? (direction === "down" ? "▼" : "▲") : ""} </div>
146+
<div>{ containerName.length > 0 ? "Loading several stuff" : "Loading" }</div>
147+
</div>
148+
)
149+
}
150+
`,
151+
options: [{ validStrategies: ['coerce'] }],
152+
},
153+
{
154+
// It shouldn't delete valid branches from ternary expressions when ["coerce", "ternary"] are only valid strategies
155+
code: `
156+
const Component = ({ elements, count }) => {
157+
return <div>{direction ? (direction === "down" ? "▼" : "▲") : ""}</div>
158+
}
159+
`,
160+
options: [{ validStrategies: ['coerce', 'ternary'] }],
161+
},
162+
{
163+
// It shouldn't report nested logical expressions when "coerce" is the only valid strategy
164+
code: `
165+
const Component = ({ direction }) => {
166+
return (
167+
<div>
168+
<div>{!!direction && direction === "down" && "▼"}</div>
169+
<div>{direction === "down" && !!direction && "▼"}</div>
170+
<div>{direction === "down" || !!direction && "▼"}</div>
171+
<div>{(!display || display === DISPLAY.WELCOME) && <span>foo</span>}</div>
172+
</div>
173+
)
174+
}
175+
`,
176+
options: [{ validStrategies: ['coerce'] }],
126177
},
127178
]),
128179

0 commit comments

Comments
 (0)