Skip to content

Commit 47e8640

Browse files
committed
[Fix] no-unused-prop-types: false positive with callback
1 parent fcfee49 commit 47e8640

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

613-
/**
614-
* Get an enclosing scope used to find `this` value by an arrow function
615-
* @param {Scope} scope Current scope
616-
* @returns {Scope} An enclosing scope used by an arrow function
617-
*/
618-
getArrowFunctionScope(scope) {
619-
scope = scope.upper;
634+
let scope = context.getScope();
620635
while (scope) {
621-
if (astUtil.isFunction(scope.block) || astUtil.isClass(scope.block)) {
622-
return scope;
636+
const node = scope.block;
637+
const statelessComponent = getStatelessComponent(node);
638+
if (statelessComponent) {
639+
return statelessComponent;
623640
}
624641
scope = scope.upper;
625642
}

lib/util/ast.js

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

135-
/**
136-
* Checks if the node is an arrow function.
137-
* @param {ASTNode} node The node to check
138-
* @return {Boolean} true if it's an arrow function
139-
*/
140-
function isArrowFunction(node) {
141-
return node.type === 'ArrowFunctionExpression';
142-
}
143-
144135
/**
145136
* Checks if the node is a class.
146137
* @param {ASTNode} node The node to check
@@ -196,7 +187,6 @@ module.exports = {
196187
getPropertyNameNode,
197188
getComponentProperties,
198189
getKeyValue,
199-
isArrowFunction,
200190
isAssignmentLHS,
201191
isClass,
202192
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)