Skip to content

Commit 7b3d728

Browse files
vedadeeptaljharb
authored andcommitted
[Fix] destructuring-assignment, component detection: improve component detection
Fixes #3119
1 parent 0743c41 commit 7b3d728

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
2121
* [`no-unstable-components`]: improve handling of objects containing render function properties ([#3111] @fizwidget)
2222
* [`prop-types`], `propTypes`: add forwardRef<>, ForwardRefRenderFunction<> prop-types ([#3112] @vedadeepta)
2323
* [`no-typos`]: prevent a crash when using private methods (@ljharb)
24+
* [`destructuring-assignment`], component detection: improve component detection ([#3122] @vedadeepta)
2425

2526
### Changed
2627
* [Tests] test on the new babel eslint parser ([#3113] @ljharb)
2728

29+
[#3122]: https://github.com/yannickcr/eslint-plugin-react/pull/3122
2830
[#3113]: https://github.com/yannickcr/eslint-plugin-react/pull/3113
2931
[#3112]: https://github.com/yannickcr/eslint-plugin-react/pull/3112
3032
[#3111]: https://github.com/yannickcr/eslint-plugin-react/pull/3111

lib/util/Components.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ function componentRule(rule, context) {
334334
return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict);
335335
},
336336

337+
isReturningOnlyNull(ASTNode) {
338+
return jsxUtil.isReturningOnlyNull(this.isCreateElement.bind(this), ASTNode, context);
339+
},
340+
337341
getPragmaComponentWrapper(node) {
338342
let isPragmaComponentWrapper;
339343
let currentNode = node;
@@ -556,6 +560,16 @@ function componentRule(rule, context) {
556560
return undefined;
557561
}
558562

563+
// case: function any() { return (props) { return not-jsx-and-not-null } }
564+
if (node.parent.type === 'ReturnStatement' && !utils.isReturningJSX(node) && !utils.isReturningOnlyNull(node)) {
565+
return undefined;
566+
}
567+
568+
// for case abc = { [someobject.somekey]: props => { ... return not-jsx } }
569+
if (node.parent && node.parent.key && node.parent.key.type === 'MemberExpression' && !utils.isReturningJSX(node) && !utils.isReturningOnlyNull(node)) {
570+
return undefined;
571+
}
572+
559573
// Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
560574
const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
561575
if (pragmaComponentWrapper) {
@@ -805,7 +819,6 @@ function componentRule(rule, context) {
805819
}
806820

807821
const component = utils.getParentComponent();
808-
809822
if (
810823
!component
811824
|| (component.parent && component.parent.type === 'JSXExpressionContainer')

lib/util/jsx.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,59 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) {
148148
return found;
149149
}
150150

151+
/**
152+
* Check if the node is returning only null values
153+
*
154+
* @param {Function} isCreateElement Function to determine if a CallExpresion is
155+
* a createElement one
156+
* @param {ASTNode} ASTnode The AST node being checked
157+
* @param {Context} context The context of `ASTNode`.
158+
* @returns {Boolean} True if the node is returning only null values
159+
*/
160+
function isReturningOnlyNull(isCreateElement, ASTnode, context) {
161+
let found = false;
162+
let foundSomethingElse = false;
163+
astUtil.traverseReturns(ASTnode, context, (node) => {
164+
// Traverse return statement
165+
astUtil.traverse(node, {
166+
enter(childNode) {
167+
const setFound = () => {
168+
found = true;
169+
this.skip();
170+
};
171+
const setFoundSomethingElse = () => {
172+
foundSomethingElse = true;
173+
this.skip();
174+
};
175+
switch (childNode.type) {
176+
case 'ReturnStatement':
177+
break;
178+
case 'ConditionalExpression':
179+
if (childNode.consequent.value === null && childNode.alternate.value === null) {
180+
setFound();
181+
}
182+
break;
183+
case 'Literal':
184+
if (childNode.value === null) {
185+
setFound();
186+
}
187+
break;
188+
default:
189+
setFoundSomethingElse();
190+
}
191+
},
192+
});
193+
});
194+
195+
return found && !foundSomethingElse;
196+
}
197+
151198
module.exports = {
152199
isDOMComponent,
153200
isFragment,
154201
isJSX,
155202
isJSXAttributeKey,
156203
isWhiteSpaces,
157204
isReturningJSX,
205+
isReturningOnlyNull,
158206
};

tests/lib/rules/destructuring-assignment.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,56 @@ const parserOptions = {
2020
const ruleTester = new RuleTester({ parserOptions });
2121
ruleTester.run('destructuring-assignment', rule, {
2222
valid: parsers.all([
23+
{
24+
code: `
25+
export const revisionStates2 = {
26+
[A.b]: props => {
27+
return props.editor !== null
28+
? 'xyz'
29+
: 'abc'
30+
},
31+
};
32+
`,
33+
},
34+
{
35+
code: `
36+
export function hof(namespace) {
37+
const initialState = {
38+
bounds: null,
39+
search: false,
40+
};
41+
return (props) => {
42+
const {x, y} = props
43+
if (y) {
44+
return <span>{y}</span>;
45+
}
46+
return <span>{x}</span>
47+
};
48+
}
49+
`,
50+
},
51+
{
52+
code: `
53+
export function hof(namespace) {
54+
const initialState = {
55+
bounds: null,
56+
search: false,
57+
};
58+
59+
return (state = initialState, action) => {
60+
if (action.type === 'ABC') {
61+
return {...state, bounds: stuff ? action.x : null};
62+
}
63+
64+
if (action.namespace !== namespace) {
65+
return state;
66+
}
67+
68+
return null
69+
};
70+
}
71+
`,
72+
},
2373
{
2474
code: `
2575
const MyComponent = ({ id, className }) => (
@@ -615,5 +665,63 @@ ruleTester.run('destructuring-assignment', rule, {
615665
},
616666
],
617667
},
668+
{
669+
code: `
670+
export const revisionStates2 = {
671+
[A.b]: props => {
672+
return props.editor !== null
673+
? <span>{props.editor}</span>
674+
: null
675+
},
676+
};
677+
`,
678+
parser: parsers.BABEL_ESLINT,
679+
errors: [
680+
{
681+
messageId: 'useDestructAssignment',
682+
data: { type: 'props' },
683+
line: 4,
684+
},
685+
{
686+
messageId: 'useDestructAssignment',
687+
data: { type: 'props' },
688+
line: 5,
689+
},
690+
],
691+
},
692+
{
693+
code: `
694+
export function hof(namespace) {
695+
const initialState = {
696+
bounds: null,
697+
search: false,
698+
};
699+
return (props) => {
700+
if (props.y) {
701+
return <span>{props.y}</span>;
702+
}
703+
return <span>{props.x}</span>
704+
};
705+
}
706+
`,
707+
parser: parsers.BABEL_ESLINT,
708+
errors: [
709+
{
710+
messageId: 'useDestructAssignment',
711+
data: { type: 'props' },
712+
line: 8,
713+
},
714+
{
715+
messageId: 'useDestructAssignment',
716+
data: { type: 'props' },
717+
line: 9,
718+
},
719+
{
720+
messageId: 'useDestructAssignment',
721+
data: { type: 'props' },
722+
line: 11,
723+
},
724+
],
725+
},
618726
]),
619727
});

0 commit comments

Comments
 (0)