Skip to content

Commit

Permalink
Merge pull request #2089 from jomasti/feature/support-react-forwardre…
Browse files Browse the repository at this point in the history
…f-memo

Support detecting React.forwardRef/React.memo
  • Loading branch information
ljharb committed Dec 27, 2018
2 parents 14451d4 + 3ce2078 commit 8c6a8e2
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 21 deletions.
2 changes: 1 addition & 1 deletion lib/rules/void-dom-elements-no-children.js
Expand Up @@ -99,7 +99,7 @@ module.exports = {
return;
}

if (!utils.isReactCreateElement(node)) {
if (!utils.isCreateElement(node)) {
return;
}

Expand Down
61 changes: 42 additions & 19 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,34 +255,33 @@ function componentRule(rule, context) {
},

/**
* Check if createElement is destructured from React import
* Check if variable is destructured from pragma import
*
* @returns {Boolean} True if createElement is destructured from React
* @param {variable} String The variable name to check
* @returns {Boolean} True if createElement is destructured from the pragma
*/
hasDestructuredReactCreateElement: function() {
isDestructuredFromPragmaImport: 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(pragma);
}
return false;
},

/**
* Checks to see if node is called within React.createElement
* Checks to see if node is called within createElement from pragma
*
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if React.createElement called
* @returns {Boolean} True if createElement called from pragma
*/
isReactCreateElement: function(node) {
const calledOnReact = (
isCreateElement: function(node) {
const calledOnPragma = (
node &&
node.callee &&
node.callee.object &&
node.callee.object.name === 'React' &&
node.callee.object.name === pragma &&
node.callee.property &&
node.callee.property.name === 'createElement'
);
Expand All @@ -291,10 +292,10 @@ function componentRule(rule, context) {
node.callee.name === 'createElement'
);

if (this.hasDestructuredReactCreateElement()) {
return calledDirectly || calledOnReact;
if (this.isDestructuredFromPragmaImport('createElement')) {
return calledDirectly || calledOnPragma;
}
return calledOnReact;
return calledOnPragma;
},

getReturnPropertyAndNode(ASTnode) {
Expand Down Expand Up @@ -356,12 +357,12 @@ function componentRule(rule, context) {
node[property] &&
jsxUtil.isJSX(node[property])
;
const returnsReactCreateElement = this.isReactCreateElement(node[property]);
const returnsPragmaCreateElement = this.isCreateElement(node[property]);

return Boolean(
returnsConditionalJSX ||
returnsJSX ||
returnsReactCreateElement
returnsPragmaCreateElement
);
},

Expand Down Expand Up @@ -394,6 +395,18 @@ function componentRule(rule, context) {
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
},

isPragmaComponentWrapper(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 === pragma;
}
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name);
},

/**
* Find a return statment in the current node
*
Expand Down Expand Up @@ -466,6 +479,9 @@ function componentRule(rule, context) {
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
if (node.parent && this.isPragmaComponentWrapper(node.parent)) {
return node.parent;
}
// Stop moving up if we reach a class or an argument (like a callback)
if (isClass || isArgument) {
return null;
Expand Down Expand Up @@ -600,6 +616,13 @@ function componentRule(rule, context) {

// Component detection instructions
const detectionInstructions = {
CallExpression: function(node) {
if (!utils.isPragmaComponentWrapper(node)) {
return;
}
components.add(node, 2);
},

ClassExpression: function(node) {
if (!utils.isES6Component(node)) {
return;
Expand Down
2 changes: 1 addition & 1 deletion lib/util/usedPropTypes.js
Expand Up @@ -429,7 +429,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
*/
function markDestructuredFunctionArgumentsAsUsed(node) {
const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern';
if (destructuring && components.get(node)) {
if (destructuring && (components.get(node) || components.get(node.parent))) {
markPropTypesAsUsed(node);
}
}
Expand Down
228 changes: 228 additions & 0 deletions tests/lib/rules/prop-types.js
Expand Up @@ -2066,6 +2066,110 @@ ruleTester.run('prop-types', rule, {
};
`,
settings: {react: {version: '16.3.0'}}
},
{
code: `
const HeaderBalance = React.memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
HeaderBalance.propTypes = {
cryptoCurrency: PropTypes.string
};
`
},
{
code: `
import React, { memo } from 'react';
const HeaderBalance = memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
HeaderBalance.propTypes = {
cryptoCurrency: PropTypes.string
};
`
},
{
code: `
import Foo, { memo } from 'foo';
const HeaderBalance = memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
HeaderBalance.propTypes = {
cryptoCurrency: PropTypes.string
};
`,
settings: {
react: {
pragma: 'Foo'
}
}
},
{
code: `
const Label = React.forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
Label.propTypes = {
text: PropTypes.string,
};
`
},
{
code: `
const Label = Foo.forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
Label.propTypes = {
text: PropTypes.string,
};
`,
settings: {
react: {
pragma: 'Foo'
}
}
},
{
code: `
import React, { forwardRef } from 'react';
const Label = forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
Label.propTypes = {
text: PropTypes.string,
};
`
},
{
code: `
import Foo, { forwardRef } from 'foo';
const Label = forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
Label.propTypes = {
text: PropTypes.string,
};
`,
settings: {
react: {
pragma: 'Foo'
}
}
}
],

Expand Down Expand Up @@ -3947,6 +4051,130 @@ 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 HeaderBalance = Foo.memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
`,
settings: {
react: {
pragma: 'Foo'
}
},
errors: [{
message: '\'cryptoCurrency\' is missing in props validation'
}]
},
{
code: `
import Foo, { memo } from 'foo';
const HeaderBalance = memo(({ cryptoCurrency }) => (
<div className="header-balance">
<div className="header-balance__balance">
BTC
{cryptoCurrency}
</div>
</div>
));
`,
settings: {
react: {
pragma: 'Foo'
}
},
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'
}]
},
{
code: `
const Label = Foo.forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
`,
settings: {
react: {
pragma: 'Foo'
}
},
errors: [{
message: '\'text\' is missing in props validation'
}]
},
{
code: `
import Foo, { forwardRef } from 'foo';
const Label = forwardRef(({ text }, ref) => {
return <div ref={ref}>{text}</div>;
});
`,
settings: {
react: {
pragma: 'Foo'
}
},
errors: [{
message: '\'text\' is missing in props validation'
}]
}
]
});

0 comments on commit 8c6a8e2

Please sign in to comment.