diff --git a/lib/util/Components.js b/lib/util/Components.js index 6d0b72dfba..7ddf088a4b 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,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; }, @@ -291,7 +292,7 @@ function componentRule(rule, context) { node.callee.name === 'createElement' ); - if (this.hasDestructuredReactCreateElement()) { + if (this.isDestructuredFromReactImport('createElement')) { return calledDirectly || calledOnReact; } return calledOnReact; @@ -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 * @@ -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 () 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..8a41063f89 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' + }] } ] });