diff --git a/lib/util/Components.js b/lib/util/Components.js index 6d0b72dfba..c0a9e4247a 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -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'); @@ -253,18 +255,16 @@ function componentRule(rule, context) { }, /** - * Check if createElement is destructured from React import + * Check if variable is destructured from React import * * @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; }, @@ -291,7 +291,7 @@ function componentRule(rule, context) { node.callee.name === 'createElement' ); - if (this.hasDestructuredReactCreateElement()) { + if (this.isDestructuredFromReactImport('createElement')) { return calledDirectly || calledOnReact; } return calledOnReact; @@ -394,6 +394,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 * @@ -463,7 +475,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 () const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer'; // Stop moving up if we reach a class or an argument (like a callback) diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index dfeb0c5cd3..d441dd3c0c 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3947,6 +3947,58 @@ ruleTester.run('prop-types', rule, { errors: [{ message: '\'page\' is missing in props validation' }] + }, + { + code: ` + const HeaderBalance = React.memo(({ cryptoCurrency }) => ( +
+
+ BTC + {cryptoCurrency} +
+
+ )); + `, + errors: [{ + message: '\'cryptoCurrency\' is missing in props validation' + }] + }, + { + code: ` + import React, { memo } from 'React'; + const HeaderBalance = memo(({ cryptoCurrency }) => ( +
+
+ BTC + {cryptoCurrency} +
+
+ )); + `, + errors: [{ + message: '\'cryptoCurrency\' is missing in props validation' + }] + }, + { + code: ` + const Label = React.forwardRef(({ text }, ref) => { + return
{text}
; + }); + `, + errors: [{ + message: '\'text\' is missing in props validation' + }] + }, + { + code: ` + import React, { forwardRef } from 'React'; + const Label = forwardRef(({ text }, ref) => { + return
{text}
; + }); + `, + errors: [{ + message: '\'text\' is missing in props validation' + }] } ] });