diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5b3498be..fccd3b7465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`destructuring-assignment`], component detection: handle default exports edge case ([#3038][] @vedadeepta) * [`no-typos`]: fix crash on private methods ([#3043][] @ljharb) * [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g) +* [`prop-types`], `propTypes`: handle React.* TypeScript types ([#3049][] @vedadeepta) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -29,6 +30,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`require-default-props`]: fix small typo ([#2994][] @evsasse) * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) +[#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 [#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048 [#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 [#3039]: https://github.com/yannickcr/eslint-plugin-react/pull/3039 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 95671b9a2f..27ed04b7be 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -23,8 +23,8 @@ function isFunctionType(node) { if (!node) return false; const nodeType = node.type; return nodeType === 'FunctionDeclaration' - || nodeType === 'FunctionExpression' - || nodeType === 'ArrowFunctionExpression'; + || nodeType === 'FunctionExpression' + || nodeType === 'ArrowFunctionExpression'; } /** @@ -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; @@ -887,7 +893,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 +915,38 @@ 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); - } - }); + if (isNodeAnnotated) { + 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)); + } } else { - markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); + // implements what's discussed here: https://github.com/yannickcr/eslint-plugin-react/issues/2777#issuecomment-683944481 + 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.indexOf(typeName) === -1) { + return; + } + + markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 689e8e14da..27dff143d2 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'] } ]), { @@ -6621,6 +6670,42 @@ ruleTester.run('prop-types', rule, { } ], parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + interface PersonProps { + test: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + interface PersonProps { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.test}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'test'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) )