Skip to content

Commit 341bb4f

Browse files
committed
Support detecting React.forwardRef/React.memo
This updates the component detection to allow for considering components wrapped in either React.forwardRef or React.memo.
1 parent 9358489 commit 341bb4f

File tree

2 files changed

+75
-10
lines changed

2 files changed

+75
-10
lines changed

lib/util/Components.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
const util = require('util');
88
const doctrine = require('doctrine');
9+
const arrayIncludes = require('array-includes');
10+
911
const variableUtil = require('./variable');
1012
const pragmaUtil = require('./pragma');
1113
const astUtil = require('./ast');
@@ -253,18 +255,17 @@ function componentRule(rule, context) {
253255
},
254256

255257
/**
256-
* Check if createElement is destructured from React import
258+
* Check if variable is destructured from React import
257259
*
260+
* @param {variable} String The variable name to check
258261
* @returns {Boolean} True if createElement is destructured from React
259262
*/
260-
hasDestructuredReactCreateElement: function() {
263+
isDestructuredFromReactImport: function(variable) {
261264
const variables = variableUtil.variablesInScope(context);
262-
const variable = variableUtil.getVariable(variables, 'createElement');
263-
if (variable) {
264-
const map = variable.scope.set;
265-
if (map.has('React')) {
266-
return true;
267-
}
265+
const variableInScope = variableUtil.getVariable(variables, variable);
266+
if (variableInScope) {
267+
const map = variableInScope.scope.set;
268+
return map.has('React');
268269
}
269270
return false;
270271
},
@@ -291,7 +292,7 @@ function componentRule(rule, context) {
291292
node.callee.name === 'createElement'
292293
);
293294

294-
if (this.hasDestructuredReactCreateElement()) {
295+
if (this.isDestructuredFromReactImport('createElement')) {
295296
return calledDirectly || calledOnReact;
296297
}
297298
return calledOnReact;
@@ -394,6 +395,18 @@ function componentRule(rule, context) {
394395
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
395396
},
396397

398+
isReactComponentWrapper(node) {
399+
if (node.type !== 'CallExpression') {
400+
return false;
401+
}
402+
const propertyNames = ['forwardRef', 'memo'];
403+
const calleeObject = node.callee.object;
404+
if (calleeObject) {
405+
return arrayIncludes(propertyNames, node.callee.property.name) && node.callee.object.name === 'React';
406+
}
407+
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromReactImport(node.callee.name);
408+
},
409+
397410
/**
398411
* Find a return statment in the current node
399412
*
@@ -463,7 +476,7 @@ function componentRule(rule, context) {
463476
const enclosingScopeParent = enclosingScope && enclosingScope.block.parent;
464477
const isClass = enclosingScope && astUtil.isClass(enclosingScope.block);
465478
const isMethod = enclosingScopeParent && enclosingScopeParent.type === 'MethodDefinition'; // Classes methods
466-
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
479+
const isArgument = node.parent && node.parent.type === 'CallExpression' && !this.isReactComponentWrapper(node.parent); // Arguments (callback, etc.)
467480
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
468481
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
469482
// Stop moving up if we reach a class or an argument (like a callback)

tests/lib/rules/prop-types.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3947,6 +3947,58 @@ ruleTester.run('prop-types', rule, {
39473947
errors: [{
39483948
message: '\'page\' is missing in props validation'
39493949
}]
3950+
},
3951+
{
3952+
code: `
3953+
const HeaderBalance = React.memo(({ cryptoCurrency }) => (
3954+
<div className="header-balance">
3955+
<div className="header-balance__balance">
3956+
BTC
3957+
{cryptoCurrency}
3958+
</div>
3959+
</div>
3960+
));
3961+
`,
3962+
errors: [{
3963+
message: '\'cryptoCurrency\' is missing in props validation'
3964+
}]
3965+
},
3966+
{
3967+
code: `
3968+
import React, { memo } from 'react';
3969+
const HeaderBalance = memo(({ cryptoCurrency }) => (
3970+
<div className="header-balance">
3971+
<div className="header-balance__balance">
3972+
BTC
3973+
{cryptoCurrency}
3974+
</div>
3975+
</div>
3976+
));
3977+
`,
3978+
errors: [{
3979+
message: '\'cryptoCurrency\' is missing in props validation'
3980+
}]
3981+
},
3982+
{
3983+
code: `
3984+
const Label = React.forwardRef(({ text }, ref) => {
3985+
return <div ref={ref}>{text}</div>;
3986+
});
3987+
`,
3988+
errors: [{
3989+
message: '\'text\' is missing in props validation'
3990+
}]
3991+
},
3992+
{
3993+
code: `
3994+
import React, { forwardRef } from 'react';
3995+
const Label = forwardRef(({ text }, ref) => {
3996+
return <div ref={ref}>{text}</div>;
3997+
});
3998+
`,
3999+
errors: [{
4000+
message: '\'text\' is missing in props validation'
4001+
}]
39504002
}
39514003
]
39524004
});

0 commit comments

Comments
 (0)