diff --git a/lib/util/annotations.js b/lib/util/annotations.js index d52fb214f1..f4a3dd2219 100644 --- a/lib/util/annotations.js +++ b/lib/util/annotations.js @@ -23,7 +23,6 @@ function isAnnotatedFunctionPropsDeclaration(node, context) { const isAnnotated = typeNode.typeAnnotation; const isDestructuredProps = typeNode.type === 'ObjectPattern'; const isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props'); - return (isAnnotated && (isDestructuredProps || isProps)); } diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 95671b9a2f..2f3805e2d0 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -100,6 +100,7 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = {customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; + const allowedGenericTypes = ['React.SFC', 'React.StatelessComponent', 'React.FunctionComponent', 'React.FC']; /** * Returns the full scope. @@ -547,6 +548,11 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; + if (!typeName && node.typeParameters && node.typeParameters.length !== 0) { + const nextNode = node.typeParameters.params[0]; + this.visitTSNode(nextNode); + return; + } } else if (astUtil.isTSInterfaceHeritage(node)) { if (!node.expression && node.id) { typeName = node.id.name; @@ -576,7 +582,6 @@ module.exports = function propTypesInstructions(context, components, utils) { */ const candidateTypes = this.sourceCode.ast.body.filter((item) => item.type === 'VariableDeclaration' && item.kind === 'type'); const declarations = flatMap(candidateTypes, (type) => type.declarations); - // we tried to find either an interface or a type with the TypeReference name const typeDeclaration = declarations.filter((dec) => dec.id.name === typeName); @@ -875,7 +880,6 @@ module.exports = function propTypesInstructions(context, components, utils) { ignorePropsValidation = true; break; } - components.set(node, { declaredPropTypes, ignorePropsValidation @@ -887,7 +891,15 @@ module.exports = function propTypesInstructions(context, components, utils) { * FunctionDeclaration, or FunctionExpression */ function markAnnotatedFunctionArgumentsAsDeclared(node) { - if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { + if (!node.params || !node.params.length) { + return; + } + + const siblingIdentifier = node.parent && node.parent.id; + const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation; + const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context); + + if (!isNodeAnnotated && !siblingHasTypeAnnotation) { return; } @@ -901,17 +913,34 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - const param = node.params[0]; - if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') { - param.typeAnnotation.typeAnnotation.types.forEach((annotation) => { - if (annotation.type === 'GenericTypeAnnotation') { - markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation)); - } else { - markPropTypesAsDeclared(node, annotation); - } - }); + // implements what's discussed here: https://github.com/yannickcr/eslint-plugin-react/issues/2777#issuecomment-683944481 + if (!isNodeAnnotated) { + const annotation = siblingIdentifier.typeAnnotation.typeAnnotation; + + if ( + annotation + && annotation.type !== 'TSTypeReference' + && annotation.typeParameters == null + ) return; + + const typeName = context.getSourceCode().getText(annotation.typeName).replace(/ /g, ''); + + if (!allowedGenericTypes.includes(typeName)) return; + + markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } else { - markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); + const param = node.params[0]; + if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') { + param.typeAnnotation.typeAnnotation.types.forEach((annotation) => { + if (annotation.type === 'GenericTypeAnnotation') { + markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation)); + } else { + markPropTypesAsDeclared(node, annotation); + } + }); + } else { + markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); + } } } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 689e8e14da..148c373f2e 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3147,6 +3147,55 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: React.FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Person: React.FunctionComponent<{ username: string }> = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type PersonProps = { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), {