Skip to content

Commit

Permalink
Support detecting React.forwardRef/React.memo
Browse files Browse the repository at this point in the history
This updates the component detection to allow for considering components
wrapped in either React.forwardRef or React.memo.
  • Loading branch information
jomasti committed Dec 16, 2018
1 parent 9358489 commit 341bb4f
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 10 deletions.
33 changes: 23 additions & 10 deletions lib/util/Components.js
Expand Up @@ -6,6 +6,8 @@

const util = require('util');
const doctrine = require('doctrine');
const arrayIncludes = require('array-includes');

const variableUtil = require('./variable');
const pragmaUtil = require('./pragma');
const astUtil = require('./ast');
Expand Down Expand Up @@ -253,18 +255,17 @@ function componentRule(rule, context) {
},

/**
* Check if createElement is destructured from React import
* Check if variable is destructured from React import
*
* @param {variable} String The variable name to check
* @returns {Boolean} True if createElement is destructured from React
*/
hasDestructuredReactCreateElement: function() {
isDestructuredFromReactImport: function(variable) {
const variables = variableUtil.variablesInScope(context);
const variable = variableUtil.getVariable(variables, 'createElement');
if (variable) {
const map = variable.scope.set;
if (map.has('React')) {
return true;
}
const variableInScope = variableUtil.getVariable(variables, variable);
if (variableInScope) {
const map = variableInScope.scope.set;
return map.has('React');
}
return false;
},
Expand All @@ -291,7 +292,7 @@ function componentRule(rule, context) {
node.callee.name === 'createElement'
);

if (this.hasDestructuredReactCreateElement()) {
if (this.isDestructuredFromReactImport('createElement')) {
return calledDirectly || calledOnReact;
}
return calledOnReact;
Expand Down Expand Up @@ -394,6 +395,18 @@ function componentRule(rule, context) {
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
},

isReactComponentWrapper(node) {
if (node.type !== 'CallExpression') {
return false;
}
const propertyNames = ['forwardRef', 'memo'];
const calleeObject = node.callee.object;
if (calleeObject) {
return arrayIncludes(propertyNames, node.callee.property.name) && node.callee.object.name === 'React';
}
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromReactImport(node.callee.name);
},

/**
* Find a return statment in the current node
*
Expand Down Expand Up @@ -463,7 +476,7 @@ function componentRule(rule, context) {
const enclosingScopeParent = enclosingScope && enclosingScope.block.parent;
const isClass = enclosingScope && astUtil.isClass(enclosingScope.block);
const isMethod = enclosingScopeParent && enclosingScopeParent.type === 'MethodDefinition'; // Classes methods
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
const isArgument = node.parent && node.parent.type === 'CallExpression' && !this.isReactComponentWrapper(node.parent); // Arguments (callback, etc.)
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
// Stop moving up if we reach a class or an argument (like a callback)
Expand Down
52 changes: 52 additions & 0 deletions tests/lib/rules/prop-types.js
Expand Up @@ -3947,6 +3947,58 @@ ruleTester.run('prop-types', rule, {
errors: [{
message: '\'page\' is missing in props validation'
}]
},
{
code: `
const HeaderBalance = React.memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
`,
errors: [{
message: '\'cryptoCurrency\' is missing in props validation'
}]
},
{
code: `
import React, { memo } from 'react';
const HeaderBalance = memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
`,
errors: [{
message: '\'cryptoCurrency\' is missing in props validation'
}]
},
{
code: `
const Label = React.forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
`,
errors: [{
message: '\'text\' is missing in props validation'
}]
},
{
code: `
import React, { forwardRef } from 'react';
const Label = forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
`,
errors: [{
message: '\'text\' is missing in props validation'
}]
}
]
});

0 comments on commit 341bb4f

Please sign in to comment.