Skip to content

Commit 66725bc

Browse files
golopotljharb
authored andcommitted
[Fix] no-unused-prop-types: false positive with callback
1 parent 3c72a49 commit 66725bc

8 files changed

+135
-51
lines changed

lib/util/Components.js

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -577,50 +577,67 @@ function componentRule(rule, context) {
577577
},
578578

579579
/**
580-
* Get the parent stateless component node from the current scope
581-
*
582-
* @returns {ASTNode} component node, null if we are not in a component
580+
* @param {ASTNode} node
581+
* @returns {boolean}
583582
*/
584-
getParentStatelessComponent() {
585-
let scope = context.getScope();
586-
while (scope) {
587-
const node = scope.block;
588-
const isFunction = /Function/.test(node.type); // Functions
589-
const isArrowFunction = astUtil.isArrowFunction(node);
590-
const enclosingScope = isArrowFunction ? utils.getArrowFunctionScope(scope) : scope;
591-
const enclosingScopeParent = enclosingScope && enclosingScope.block.parent;
592-
const isClass = enclosingScope && astUtil.isClass(enclosingScope.block);
593-
const isMethod = enclosingScopeParent && enclosingScopeParent.type === 'MethodDefinition'; // Classes methods
594-
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
595-
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
596-
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
597-
const pragmaComponentWrapper = this.getPragmaComponentWrapper(node);
598-
if (isFunction && pragmaComponentWrapper) {
599-
return pragmaComponentWrapper;
583+
isInAllowedPositionForComponent(node) {
584+
switch (node.parent.type) {
585+
case 'VariableDeclarator':
586+
case 'AssignmentExpression':
587+
case 'Property':
588+
case 'ReturnStatement':
589+
case 'ExportDefaultDeclaration': {
590+
return true;
600591
}
601-
// Stop moving up if we reach a class or an argument (like a callback)
602-
if (isClass || isArgument) {
603-
return null;
592+
case 'SequenceExpression': {
593+
return utils.isInAllowedPositionForComponent(node.parent) &&
594+
node === node.parent.expressions[node.parent.expressions.length - 1];
604595
}
605-
// Return the node if it is a function that is not a class method and is not inside a JSX Element
606-
if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSXOrNull(node)) {
596+
default:
597+
return false;
598+
}
599+
},
600+
601+
/**
602+
* Get node if node is a stateless component, or node.parent in cases like
603+
* `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
604+
* @param {ASTNode} node
605+
* @returns {ASTNode | undefined}
606+
*/
607+
getStatelessComponent(node) {
608+
if (node.type === 'FunctionDeclaration') {
609+
if (utils.isReturningJSXOrNull(node)) {
607610
return node;
608611
}
609-
scope = scope.upper;
610612
}
611-
return null;
613+
614+
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
615+
if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
616+
return node;
617+
}
618+
619+
// Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
620+
const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
621+
if (pragmaComponentWrapper) {
622+
return pragmaComponentWrapper;
623+
}
624+
}
625+
626+
return undefined;
612627
},
613628

614629
/**
615-
* Get an enclosing scope used to find `this` value by an arrow function
616-
* @param {Scope} scope Current scope
617-
* @returns {Scope} An enclosing scope used by an arrow function
630+
* Get the parent stateless component node from the current scope
631+
*
632+
* @returns {ASTNode} component node, null if we are not in a component
618633
*/
619-
getArrowFunctionScope(scope) {
620-
scope = scope.upper;
634+
getParentStatelessComponent() {
635+
let scope = context.getScope();
621636
while (scope) {
622-
if (astUtil.isFunction(scope.block) || astUtil.isClass(scope.block)) {
623-
return scope;
637+
const node = scope.block;
638+
const statelessComponent = utils.getStatelessComponent(node);
639+
if (statelessComponent) {
640+
return statelessComponent;
624641
}
625642
scope = scope.upper;
626643
}

lib/util/ast.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,6 @@ function isFunction(node) {
133133
return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
134134
}
135135

136-
/**
137-
* Checks if the node is an arrow function.
138-
* @param {ASTNode} node The node to check
139-
* @return {Boolean} true if it's an arrow function
140-
*/
141-
function isArrowFunction(node) {
142-
return node.type === 'ArrowFunctionExpression';
143-
}
144-
145136
/**
146137
* Checks if the node is a class.
147138
* @param {ASTNode} node The node to check
@@ -198,7 +189,6 @@ module.exports = {
198189
getPropertyNameNode,
199190
getComponentProperties,
200191
getKeyValue,
201-
isArrowFunction,
202192
isAssignmentLHS,
203193
isClass,
204194
isFunction,

tests/lib/rules/destructuring-assignment.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,29 @@ ruleTester.run('destructuring-assignment', rule, {
256256
errors: [
257257
{message: 'Must use destructuring props assignment'}
258258
]
259+
}, {
260+
code: `
261+
module.exports = {
262+
Foo(props) {
263+
return <p>{props.a}</p>;
264+
}
265+
}
266+
`,
267+
errors: [{message: 'Must use destructuring props assignment'}]
268+
}, {
269+
code: `
270+
export default function Foo(props) {
271+
return <p>{props.a}</p>;
272+
}
273+
`,
274+
errors: [{message: 'Must use destructuring props assignment'}]
275+
}, {
276+
code: `
277+
function hof() {
278+
return (props) => <p>{props.a}</p>;
279+
}
280+
`,
281+
errors: [{message: 'Must use destructuring props assignment'}]
259282
}, {
260283
code: `const Foo = class extends React.PureComponent {
261284
render() {

tests/lib/rules/display-name.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,18 @@ ruleTester.run('display-name', rule, {
661661
errors: [{
662662
message: 'Component definition is missing display name'
663663
}]
664+
}, {
665+
code: `
666+
function Hof() {
667+
return function () {
668+
return <div />
669+
}
670+
}
671+
`,
672+
parser: parsers.BABEL_ESLINT,
673+
errors: [{
674+
message: 'Component definition is missing display name'
675+
}]
664676
}, {
665677
code: `
666678
import React, { createElement } from "react";

tests/lib/rules/no-multi-comp.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,26 @@ ruleTester.run('no-multi-comp', rule, {
312312
message: 'Declare only one React component per file',
313313
line: 6
314314
}]
315-
}, {
315+
},
316+
{
317+
code: `
318+
exports.Foo = function Foo() {
319+
return <></>
320+
}
321+
322+
exports.createSomeComponent = function createSomeComponent(opts) {
323+
return function Foo() {
324+
return <>{opts.a}</>
325+
}
326+
}
327+
`,
328+
parser: parsers.BABEL_ESLINT,
329+
errors: [{
330+
message: 'Declare only one React component per file',
331+
line: 7
332+
}]
333+
},
334+
{
316335
code: `
317336
class StoreListItem extends React.PureComponent {
318337
// A bunch of stuff here

tests/lib/rules/no-this-in-sfc.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,6 @@ ruleTester.run('no-this-in-sfc', rule, {
217217
return <div onClick={onClick}>{this.props.foo}</div>;
218218
}`,
219219
errors: [{message: ERROR_MESSAGE}, {message: ERROR_MESSAGE}]
220-
}, {
221-
code: `
222-
() => {
223-
this.something();
224-
return null;
225-
}`,
226-
errors: [{message: ERROR_MESSAGE}]
227220
}, {
228221
code: `
229222
class Foo {

tests/lib/rules/no-unused-prop-types.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,22 @@ ruleTester.run('no-unused-prop-types', rule, {
18561856
' previousPage: PropTypes.func.isRequired,',
18571857
'};'
18581858
].join('\n')
1859+
}, {
1860+
// issue 2350
1861+
code: `
1862+
function Foo(props) {
1863+
useEffect(() => {
1864+
const { a } = props;
1865+
document.title = a;
1866+
});
1867+
1868+
return <p/>;
1869+
}
1870+
1871+
Foo.propTypes = {
1872+
a: PropTypes.string,
1873+
}
1874+
`
18591875
}, {
18601876
code: [
18611877
'class Hello extends Component {',

tests/lib/rules/require-default-props.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,20 @@ ruleTester.run('require-default-props', rule, {
22252225
errors: [{
22262226
message: 'propType "usedProp" is not required, but has no corresponding defaultProps declaration.'
22272227
}]
2228+
},
2229+
{
2230+
code: `
2231+
Foo.propTypes = {
2232+
a: PropTypes.string,
2233+
}
2234+
2235+
export default function Foo(props) {
2236+
return <p>{props.a}</p>
2237+
};
2238+
`,
2239+
errors: [{
2240+
message: 'propType "a" is not required, but has no corresponding defaultProps declaration.'
2241+
}]
22282242
}
22292243
]
22302244
});

0 commit comments

Comments
 (0)