From 13a863b0c167901b9663be0df0d9540a24d6a4d7 Mon Sep 17 00:00:00 2001 From: Antoine Date: Sun, 7 Jun 2020 13:08:40 +0200 Subject: [PATCH] [Fix] `no-unused-prop-types`: fix with typescript eslint parser Fixes #2569. Co-authored-by: Antoine Co-authored-by: Jordan Harband --- lib/util/propTypes.js | 46 +++++++++ package.json | 1 + tests/lib/rules/no-unused-prop-types.js | 127 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 1a2d21636f..f8579bbfcb 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -4,6 +4,8 @@ 'use strict'; +const flatMap = require('array.prototype.flatmap'); + const annotations = require('./annotations'); const propsUtil = require('./props'); const variableUtil = require('./variable'); @@ -281,6 +283,47 @@ module.exports = function propTypesInstructions(context, components, utils) { return ignorePropsValidation; } + function declarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes) { + let foundDeclaredPropertiesList = []; + if (propTypes.typeAnnotation.type === 'TSTypeReference') { + const typeName = propTypes.typeAnnotation.typeName.name; + if (!typeName) { + return true; + } + // the game here is to find the type declaration in the code + const candidateTypes = context.getSourceCode().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.find((dec) => dec.id.name === typeName); + const interfaceDeclaration = context.getSourceCode().ast.body.find((item) => item.type === 'TSInterfaceDeclaration' && item.id.name === typeName); + + if (typeDeclaration) { + foundDeclaredPropertiesList = typeDeclaration.init.members; + } else if (interfaceDeclaration) { + foundDeclaredPropertiesList = interfaceDeclaration.body.body; + } else { + // type not found, for example can be an exported type, etc. Can issue a warning in the future. + return true; + } + } else if (propTypes.typeAnnotation.type === 'TSTypeLiteral') { + foundDeclaredPropertiesList = propTypes.typeAnnotation.members; + } else { + // weird cases such as TSTypeFunction + return true; + } + + foundDeclaredPropertiesList.forEach((tsPropertySignature) => { + declaredPropTypes[tsPropertySignature.key.name] = { + fullName: tsPropertySignature.key.name, + name: tsPropertySignature.key.name, + node: tsPropertySignature, + isRequired: !tsPropertySignature.optional + }; + }); + return false; + } + /** * Marks all props found inside IntersectionTypeAnnotation as declared. * Since InterSectionTypeAnnotations can be nested, this handles recursively. @@ -547,6 +590,9 @@ module.exports = function propTypesInstructions(context, components, utils) { ignorePropsValidation = true; } break; + case 'TSTypeAnnotation': + ignorePropsValidation = declarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes); + break; case null: break; default: diff --git a/package.json b/package.json index f5bcc357bc..c7ab0d7278 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "bugs": "https://github.com/yannickcr/eslint-plugin-react/issues", "dependencies": { "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", "doctrine": "^2.1.0", "has": "^1.0.3", "jsx-ast-utils": "^2.4.1", diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 78b36eee4a..9d5bbcb9b6 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -3211,6 +3211,42 @@ ruleTester.run('no-unused-prop-types', rule, { 'export default Thing;' ].join('\n'), parser: parsers.TYPESCRIPT_ESLINT + }, + // this test checks that there is no crash if no declaration is found (TSTypeLiteral). + { + code: [ + 'const Hello = (props: {firstname: string, lastname: string}) => {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, + // this test checks that there is no crash if no declaration is found (TSTypeReference). + { + code: [ + 'const Hello = (props: UnfoundProps) => {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, + { + // Omit, etc, cannot be handled, but must not trigger an error + code: [ + 'const Hello = (props: Omit<{a: string, b: string, c: string}, "a">) => {', + ' return
{props.b}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, + { + // neither TSTypeReference or TSTypeLiteral, we do nothing. Weird case + code: [ + 'const Hello = (props: () => any) => {', + ' return
{props.firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT } ], @@ -5312,6 +5348,97 @@ ruleTester.run('no-unused-prop-types', rule, { }, { message: '\'thisPropsPropUnused\' PropType is defined but prop is never used' }] + }, { + code: [ + 'type Person = {', + ' lastname: string', + '};', + 'const Hello = (props: Person) => {', + ' return
Hello {props.firstname}
;', + '}' + ].join('\n'), + parser: parsers.BABEL_ESLINT, + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {', + ' lastname: string', + '};', + 'const Hello = (props: Person) => {', + ' return
Hello {props.firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT, + errors: [ + { + message: '\'lastname\' PropType is defined but prop is never used' + } + ] + }, + { + code: [ + 'type Person = {', + ' lastname?: string', + '};', + 'const Hello = (props: Person) => {', + ' return
Hello {props.firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT, + errors: [ + { + message: '\'lastname\' PropType is defined but prop is never used' + } + ] + }, + { + code: [ + 'type Person = {', + ' firstname: string', + ' lastname: string', + '};', + 'const Hello = ({firstname}: Person) => {', + ' return
Hello {firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT, + errors: [ + { + message: '\'lastname\' PropType is defined but prop is never used' + } + ] + }, + { + code: [ + 'interface Person {', + ' firstname: string', + ' lastname: string', + '};', + 'const Hello = ({firstname}: Person) => {', + ' return
Hello {firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT, + errors: [ + { + message: '\'lastname\' PropType is defined but prop is never used' + } + ] + }, + { + code: [ + 'const Hello = ({firstname}: {firstname: string, lastname: string}) => {', + ' return
Hello {firstname}
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT, + errors: [ + { + message: '\'lastname\' PropType is defined but prop is never used' + } + ] } /* , {