From 90d2cdf1fcb9bd490543ddb87aa16915b75ceb84 Mon Sep 17 00:00:00 2001 From: Hank Chen Date: Mon, 6 Jul 2020 14:00:23 +0800 Subject: [PATCH] [Fix] `no-typos`/`no-unused-prop-types`/propType detection: Support typescript props interface extension and TSTypeAliasDeclaration Fixes #2654. Fixes #2719. Fixes #2703. Co-authored-by: Hank Chen Co-authored-by: Jordan Harband --- lib/util/ast.js | 72 +- lib/util/propTypes.js | 315 ++++- tests/lib/rules/no-unused-prop-types.js | 1408 +++++++++++++++++++---- tests/lib/rules/prop-types.js | 848 ++++++++++++-- 4 files changed, 2251 insertions(+), 392 deletions(-) diff --git a/lib/util/ast.js b/lib/util/ast.js index bd4d71b1c3..edf4c38237 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -201,6 +201,66 @@ function unwrapTSAsExpression(node) { return node; } +function isTSTypeReference(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeReference'; +} + +function isTSTypeAnnotation(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeAnnotation'; +} + +function isTSTypeLiteral(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeLiteral'; +} + +function isTSIntersectionType(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSIntersectionType'; +} + +function isTSInterfaceHeritage(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSInterfaceHeritage'; +} + +function isTSInterfaceDeclaration(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSInterfaceDeclaration'; +} + +function isTSTypeAliasDeclaration(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeAliasDeclaration'; +} + +function isTSParenthesizedType(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeAliasDeclaration'; +} + +function isTSFunctionType(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSFunctionType'; +} + +function isTSTypeQuery(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'TSTypeQuery'; +} + module.exports = { findReturnStatement, getFirstNodeInLine, @@ -213,5 +273,15 @@ module.exports = { isFunction, isFunctionLikeExpression, isNodeFirstInLine, - unwrapTSAsExpression + unwrapTSAsExpression, + isTSTypeReference, + isTSTypeAnnotation, + isTSTypeLiteral, + isTSIntersectionType, + isTSInterfaceHeritage, + isTSInterfaceDeclaration, + isTSTypeAliasDeclaration, + isTSParenthesizedType, + isTSFunctionType, + isTSTypeQuery }; diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index bf9103a905..6def79afc5 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -11,7 +11,20 @@ const propsUtil = require('./props'); const variableUtil = require('./variable'); const versionUtil = require('./version'); const propWrapperUtil = require('./propWrapper'); -const getKeyValue = require('./ast').getKeyValue; +const astUtil = require('./ast'); + +/** + * Check if node is function type. + * @param {ASTNode} node + * @returns {Boolean} + */ +function isFunctionType(node) { + if (!node) return false; + const nodeType = node.type; + return nodeType === 'FunctionDeclaration' + || nodeType === 'FunctionExpression' + || nodeType === 'ArrowFunctionExpression'; +} /** * Checks if we are declaring a props as a generic type in a flow-annotated class. @@ -41,7 +54,7 @@ function iterateProperties(context, properties, fn, handleSpreadFn) { if (properties && properties.length && typeof fn === 'function') { for (let i = 0, j = properties.length; i < j; i++) { const node = properties[i]; - const key = getKeyValue(context, node); + const key = astUtil.getKeyValue(context, node); if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') { handleSpreadFn(node.argument); @@ -147,7 +160,7 @@ module.exports = function propTypesInstructions(context, components, utils) { } }, (spreadNode) => { - const key = getKeyValue(context, spreadNode); + const key = astUtil.getKeyValue(context, spreadNode); const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen); if (!types.children) { containsUnresolvedObjectTypeSpread = true; @@ -268,7 +281,7 @@ module.exports = function propTypesInstructions(context, components, utils) { types.isRequired = !propNode.optional; declaredPropTypes[key] = types; }, (spreadNode) => { - const key = getKeyValue(context, spreadNode); + const key = astUtil.getKeyValue(context, spreadNode); const spreadAnnotation = getInTypeScope(key); if (!spreadAnnotation) { ignorePropsValidation = true; @@ -281,57 +294,6 @@ 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((tsInterfaceBody) => { - if (tsInterfaceBody.type === 'TSPropertySignature') { - let accessor = 'name'; - if (tsInterfaceBody.key.type === 'Literal') { - if (typeof tsInterfaceBody.key.value === 'number') { - accessor = 'raw'; - } else { - accessor = 'value'; - } - } - declaredPropTypes[tsInterfaceBody.key[accessor]] = { - fullName: tsInterfaceBody.key[accessor], - name: tsInterfaceBody.key[accessor], - node: tsInterfaceBody, - isRequired: !tsInterfaceBody.optional - }; - } - }); - return false; - } - /** * Marks all props found inside IntersectionTypeAnnotation as declared. * Since InterSectionTypeAnnotations can be nested, this handles recursively. @@ -523,6 +485,240 @@ module.exports = function propTypesInstructions(context, components, utils) { return {}; } + class DeclarePropTypesForTSTypeAnnotation { + constructor(propTypes, declaredPropTypes) { + this.propTypes = propTypes; + this.declaredPropTypes = declaredPropTypes; + this.foundDeclaredPropertiesList = []; + this.referenceNameMap = new Set(); + this.sourceCode = context.getSourceCode(); + this.shouldIgnorePropTypes = false; + this.startWithTSTypeAnnotation(); + this.endAndStructDeclaredPropTypes(); + } + + startWithTSTypeAnnotation() { + if (astUtil.isTSTypeAnnotation(this.propTypes)) { + const typeAnnotation = this.propTypes.typeAnnotation; + this.visitTSNode(typeAnnotation); + } else { + // weird cases such as TSTypeFunction + this.shouldIgnorePropTypes = true; + } + } + + /** + * The node will be distribute to different function. + * @param {ASTNode} node + */ + visitTSNode(node) { + if (!node) return; + if (astUtil.isTSTypeReference(node)) { + this.searchDeclarationByName(node); + } else if (astUtil.isTSInterfaceHeritage(node)) { + this.searchDeclarationByName(node); + } else if (astUtil.isTSTypeLiteral(node)) { + // Check node is an object literal + if (Array.isArray(node.members)) { + this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.members); + } + } else if (astUtil.isTSIntersectionType(node)) { + this.convertIntersectionTypeToPropTypes(node); + } else if (astUtil.isTSParenthesizedType(node)) { + const typeAnnotation = node.typeAnnotation; + if (astUtil.isTSTypeLiteral(typeAnnotation)) { + // Check node is an object literal + if (Array.isArray(node.typeAnnotation.members)) { + this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList + .concat(node.typeAnnotation.members); + } + } + } else { + this.shouldIgnorePropTypes = true; + } + } + + /** + * Search TSInterfaceDeclaration or TSTypeAliasDeclaration, + * by using TSTypeReference and TSInterfaceHeritage name. + * @param {ASTNode} node + */ + searchDeclarationByName(node) { + let typeName; + if (astUtil.isTSTypeReference(node)) { + typeName = node.typeName.name; + } else if (astUtil.isTSInterfaceHeritage(node)) { + if (!node.expression && node.id) { + typeName = node.id.name; + } else { + typeName = node.expression.name; + } + } + if (!typeName) { + this.shouldIgnorePropTypes = true; + return; + } + if (typeName === 'ReturnType') { + this.convertReturnTypeToPropTypes(node); + return; + } + // Prevent recursive inheritance will cause maximum callstack. + if (this.referenceNameMap.has(typeName)) { + this.shouldIgnorePropTypes = true; + return; + } + // Add typeName to Set and consider it as traversed. + this.referenceNameMap.add(typeName); + + /** + * From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser + * Need to be deprecated after remove typescript-eslint-parser support. + */ + 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); + + const interfaceDeclarations = this.sourceCode.ast.body + .filter( + (item) => (astUtil.isTSInterfaceDeclaration(item) + || astUtil.isTSTypeAliasDeclaration(item)) + && item.id.name === typeName); + if (typeDeclaration.length !== 0) { + typeDeclaration.map((t) => t.init).forEach(this.visitTSNode, this); + } else if (interfaceDeclarations.length !== 0) { + interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this); + } else { + this.shouldIgnorePropTypes = true; + } + } + + /** + * Traverse TSInterfaceDeclaration and TSTypeAliasDeclaration + * which retrieve from function searchDeclarationByName; + * @param {ASTNode} node + */ + traverseDeclaredInterfaceOrTypeAlias(node) { + if (astUtil.isTSInterfaceDeclaration(node)) { + // Handle TSInterfaceDeclaration interface Props { name: string, id: number}, should put in properties list directly; + this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.body.body); + } + // Handle TSTypeAliasDeclaration type Props = {name:string} + if (astUtil.isTSTypeAliasDeclaration(node)) { + const typeAnnotation = node.typeAnnotation; + this.visitTSNode(typeAnnotation); + } + if (Array.isArray(node.extends)) { + node.extends.forEach(this.visitTSNode, this); + // This line is trying to handle typescript-eslint-parser + // typescript-eslint-parser extension is name as heritage + } else if (Array.isArray(node.heritage)) { + node.heritage.forEach(this.visitTSNode, this); + } + } + + convertIntersectionTypeToPropTypes(node) { + if (!node) return; + if (Array.isArray(node.types)) { + node.types.forEach(this.visitTSNode, this); + } else { + this.shouldIgnorePropTypes = true; + } + } + + convertReturnTypeToPropTypes(node) { + // ReturnType should always have one parameter + if (node.typeParameters) { + if (node.typeParameters.params.length === 1) { + let returnType = node.typeParameters.params[0]; + // This line is trying to handle typescript-eslint-parser + // typescript-eslint-parser TSTypeQuery is wrapped by TSTypeReference + if (astUtil.isTSTypeReference(returnType)) { + returnType = returnType.typeName; + } + // Handle ReturnType + if (astUtil.isTSTypeQuery(returnType)) { + const returnTypeFunction = flatMap(this.sourceCode.ast.body + .filter((item) => item.type === 'VariableDeclaration' + && item.declarations.find((dec) => dec.id.name === returnType.exprName.name) + ), (type) => type.declarations).map((dec) => dec.init); + + if (Array.isArray(returnTypeFunction)) { + if (returnTypeFunction.length === 0) { + // Cannot find identifier in current scope. It might be an exported type. + this.shouldIgnorePropTypes = true; + return; + } + returnTypeFunction.forEach((func) => { + if (isFunctionType(func)) { + let res = func.body; + if (res.type === 'BlockStatement') { + res = astUtil.findReturnStatement(func); + if (res) { + res = res.argument; + } + } + switch (res.type) { + case 'ObjectExpression': + iterateProperties(context, res.properties, (key, value, propNode) => { + const types = buildReactDeclarationTypes(value, key); + types.fullName = key; + types.name = key; + types.node = propNode; + types.isRequired = propsUtil.isRequiredPropType(value); + this.declaredPropTypes[key] = types; + }); + break; + default: + } + } + }); + return; + } + } + // Handle ReturnType<()=>returnType> + if (astUtil.isTSFunctionType(returnType)) { + if (astUtil.isTSTypeAnnotation(returnType.returnType)) { + const returnTypeAnnotation = returnType.returnType.typeAnnotation; + this.visitTSNode(returnTypeAnnotation); + return; + } + // This line is trying to handle typescript-eslint-parser + // typescript-eslint-parser TSFunction name returnType as typeAnnotation + if (astUtil.isTSTypeAnnotation(returnType.typeAnnotation)) { + const returnTypeAnnotation = returnType.typeAnnotation.typeAnnotation; + this.visitTSNode(returnTypeAnnotation); + return; + } + } + } + } + this.shouldIgnorePropTypes = true; + } + + endAndStructDeclaredPropTypes() { + this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => { + if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) { + let accessor = 'name'; + if (tsInterfaceBody.key.type === 'Literal') { + if (typeof tsInterfaceBody.key.value === 'number') { + accessor = 'raw'; + } else { + accessor = 'value'; + } + } + this.declaredPropTypes[tsInterfaceBody.key[accessor]] = { + fullName: tsInterfaceBody.key[accessor], + name: tsInterfaceBody.key[accessor], + node: tsInterfaceBody, + isRequired: !tsInterfaceBody.optional + }; + } + }); + } + } + /** * Mark a prop type as declared * @param {ASTNode} node The AST node being checked. @@ -534,7 +730,7 @@ module.exports = function propTypesInstructions(context, components, utils) { componentNode = componentNode.parent; } const component = components.get(componentNode); - const declaredPropTypes = component && component.declaredPropTypes || {}; + let declaredPropTypes = component && component.declaredPropTypes || {}; let ignorePropsValidation = component && component.ignorePropsValidation || false; switch (propTypes && propTypes.type) { case 'ObjectTypeAnnotation': @@ -648,8 +844,11 @@ module.exports = function propTypesInstructions(context, components, utils) { ignorePropsValidation = true; } break; - case 'TSTypeAnnotation': - ignorePropsValidation = declarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes); + case 'TSTypeAnnotation': { + const tsTypeAnnotation = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes); + ignorePropsValidation = tsTypeAnnotation.shouldIgnorePropTypes; + declaredPropTypes = tsTypeAnnotation.declaredPropTypes; + } break; case null: break; diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index ae472dfea3..ad427e04fc 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -35,7 +35,7 @@ const settings = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('no-unused-prop-types', rule, { - valid: [ + valid: [].concat( { code: [ 'var Hello = createReactClass({', @@ -3212,224 +3212,466 @@ ruleTester.run('no-unused-prop-types', rule, { Foo.defaultProps = Object.assign({}); ` - }, { - code: ` - const Foo = (props) => { - const { foo } = props as unknown; - (props as unknown).bar as unknown; + }, + parsers.TS([ + { + code: ` + const Foo = (props) => { + const { foo } = props as unknown; + (props as unknown).bar as unknown; - return <>; - }; + return <>; + }; - Foo.propTypes = { - foo, - bar, - }; - `, - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: ` - class Foo extends React.Component { - static propTypes = { - prevProp, - nextProp, - setStateProp, - thisPropsAliasDestructProp, - thisPropsAliasProp, - thisDestructPropsAliasDestructProp, - thisDestructPropsAliasProp, - thisDestructPropsDestructProp, - thisPropsDestructProp, - thisPropsProp, + Foo.propTypes = { + foo, + bar, }; + `, + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: ` + const Foo = (props) => { + const { foo } = props as unknown; + (props as unknown).bar as unknown; - componentDidUpdate(prevProps) { - (prevProps as unknown).prevProp as unknown; - } + return <>; + }; - shouldComponentUpdate(nextProps) { - (nextProps as unknown).nextProp as unknown; - } + Foo.propTypes = { + foo, + bar, + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: ` + class Foo extends React.Component { + static propTypes = { + prevProp, + nextProp, + setStateProp, + thisPropsAliasDestructProp, + thisPropsAliasProp, + thisDestructPropsAliasDestructProp, + thisDestructPropsAliasProp, + thisDestructPropsDestructProp, + thisPropsDestructProp, + thisPropsProp, + }; - stateProps() { - ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); - } + componentDidUpdate(prevProps) { + (prevProps as unknown).prevProp as unknown; + } - thisPropsAlias() { - const props = (this as unknown).props as unknown; + shouldComponentUpdate(nextProps) { + (nextProps as unknown).nextProp as unknown; + } - const { thisPropsAliasDestructProp } = props as unknown; - (props as unknown).thisPropsAliasProp as unknown; - } + stateProps() { + ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); + } - thisDestructPropsAlias() { - const { props } = this as unknown; + thisPropsAlias() { + const props = (this as unknown).props as unknown; - const { thisDestructPropsAliasDestructProp } = props as unknown; - (props as unknown).thisDestructPropsAliasProp as unknown; + const { thisPropsAliasDestructProp } = props as unknown; + (props as unknown).thisPropsAliasProp as unknown; + } + + thisDestructPropsAlias() { + const { props } = this as unknown; + + const { thisDestructPropsAliasDestructProp } = props as unknown; + (props as unknown).thisDestructPropsAliasProp as unknown; + } + + render() { + const { props: { thisDestructPropsDestructProp } } = this as unknown; + const { thisPropsDestructProp } = (this as unknown).props as unknown; + ((this as unknown).props as unknown).thisPropsProp as unknown; + + return null; + } } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + class Foo extends React.Component { + static propTypes = { + prevProp, + nextProp, + setStateProp, + thisPropsAliasDestructProp, + thisPropsAliasProp, + thisDestructPropsAliasDestructProp, + thisDestructPropsAliasProp, + thisDestructPropsDestructProp, + thisPropsDestructProp, + thisPropsProp, + }; - render() { - const { props: { thisDestructPropsDestructProp } } = this as unknown; - const { thisPropsDestructProp } = (this as unknown).props as unknown; - ((this as unknown).props as unknown).thisPropsProp as unknown; + componentDidUpdate(prevProps) { + (prevProps as unknown).prevProp as unknown; + } - return null; + shouldComponentUpdate(nextProps) { + (nextProps as unknown).nextProp as unknown; + } + + stateProps() { + ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); + } + + thisPropsAlias() { + const props = (this as unknown).props as unknown; + + const { thisPropsAliasDestructProp } = props as unknown; + (props as unknown).thisPropsAliasProp as unknown; + } + + thisDestructPropsAlias() { + const { props } = this as unknown; + + const { thisDestructPropsAliasDestructProp } = props as unknown; + (props as unknown).thisDestructPropsAliasProp as unknown; + } + + render() { + const { props: { thisDestructPropsDestructProp } } = this as unknown; + const { thisPropsDestructProp } = (this as unknown).props as unknown; + ((this as unknown).props as unknown).thisPropsProp as unknown; + + return null; + } } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: [ + 'declare class Thing {', + ' constructor({ id }: { id: string });', + '}', + 'export default Thing;' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: [ + 'declare class Thing {', + ' constructor({ id }: { id: string });', + '}', + '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 + }, + { + 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 + }, + { + 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 + }, + { + // 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 + }, + { + // neither TSTypeReference or TSTypeLiteral, we do nothing. Weird case + code: [ + 'const Hello = (props: () => any) => {', + ' return
{props.firstname}
;', + '}' + ].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 + }, { + // neither TSTypeReference or TSTypeLiteral, we do nothing. Weird case + code: [ + 'const Hello = (props: () => any) => {', + ' return
{props?.firstname}
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' \'aria-label\': string;', + '}', + 'export default function Component({', + ' \'aria-label\': ariaLabel,', + '}: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' \'aria-label\': string;', + '}', + 'export default function Component({', + ' \'aria-label\': ariaLabel,', + '}: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' [\'aria-label\']: string;', + '}', + 'export default function Component({', + ' [\'aria-label\']: ariaLabel,', + '}: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' [\'aria-label\']: string;', + '}', + 'export default function Component({', + ' [\'aria-label\']: ariaLabel,', + '}: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' [\'1234\']: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' [\'1234\']: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + 'const handleVerifySubmit = ({', + ' otp,', + ' }) => {', + ' dispatch(', + ' verifyOTPPhone({', + ' otp,', + ' }),', + ' );', + '};', + 'return
;', + '}' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(', + ' props ', + ': Props): JSX.Element {', + 'const handleVerifySubmit = ({', + ' otp,', + ' }) => {', + ' dispatch(', + ' verifyOTPPhone({', + ' otp,', + ' }),', + ' );', + '};', + 'return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, { + code: [ + 'interface Props {', + ' foo: string;', + '}', + 'const Component = (props: Props) => (', + '
{(()=> {return props.foo})()}
', + ')', + 'export default Component' + ].join('\n'), + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: [ + 'interface Props {', + ' foo: string;', + '}', + 'const Component = (props: Props) => (', + '
{(()=> {return props.foo})()}
', + ')', + 'export default Component' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type User = { + user: string; } - `, - parser: parsers.TYPESCRIPT_ESLINT - }, - { - code: [ - 'declare class Thing {', - ' constructor({ id }: { id: string });', - '}', - '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 - }, - { - // neither TSTypeReference or TSTypeLiteral, we do nothing. Weird case - code: [ - 'const Hello = (props: () => any) => {', - ' return
{props?.firstname}
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' \'aria-label\': string;', - '}', - 'export default function Component({', - ' \'aria-label\': ariaLabel,', - '}: Props): JSX.Element {', - ' return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' [\'aria-label\']: string;', - '}', - 'export default function Component({', - ' [\'aria-label\']: ariaLabel,', - '}: Props): JSX.Element {', - ' return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' [1234]: string;', - '}', - 'export default function Component(', - ' props ', - ': Props): JSX.Element {', - ' return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' [\'1234\']: string;', - '}', - 'export default function Component(', - ' props ', - ': Props): JSX.Element {', - ' return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' [1234]: string;', - '}', - 'export default function Component(', - ' props ', - ': Props): JSX.Element {', - ' return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, - { - code: [ - 'interface Props {', - ' [1234]: string;', - '}', - 'export default function Component(', - ' props ', - ': Props): JSX.Element {', - 'const handleVerifySubmit = ({', - ' otp,', - ' }) => {', - ' dispatch(', - ' verifyOTPPhone({', - ' otp,', - ' }),', - ' );', - '};', - 'return
;', - '}' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - }, { - code: [ - 'interface Props {', - ' foo: string;', - '}', - 'const Component = (props: Props) => (', - '
{(()=> {return props.foo})()}
', - ')', - 'export default Component' - ].join('\n'), - parser: parsers.TYPESCRIPT_ESLINT - } - ], + + type Props = User; + + export default (props: Props) => { + return
{props.user}
; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type User = { + user: string; + } + + type Props = User & UserProps; + + export default (props: Props) => { + return
{props.user}
; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + } + ]) + ), - invalid: [ + invalid: [].concat( { code: [ 'var Hello = createReactClass({', @@ -5434,7 +5676,7 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [{ message: '\'unUsedProp\' PropType is defined but prop is never used' }] - }, { + }, parsers.TS([{ code: ` const Foo = (props) => { const { foo } = props as unknown; @@ -5454,6 +5696,26 @@ ruleTester.run('no-unused-prop-types', rule, { }, { message: '\'barUnused\' PropType is defined but prop is never used' }] + }, { + code: ` + const Foo = (props) => { + const { foo } = props as unknown; + (props as unknown).bar as unknown; + + return <>; + }; + + Foo.propTypes = { + fooUnused, + barUnused, + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'fooUnused\' PropType is defined but prop is never used' + }, { + message: '\'barUnused\' PropType is defined but prop is never used' + }] }, { code: ` class Foo extends React.Component { @@ -5528,25 +5790,85 @@ 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, + code: ` + class Foo extends React.Component { + static propTypes = { + prevPropUnused, + nextPropUnused, + setStatePropUnused, + thisPropsAliasDestructPropUnused, + thisPropsAliasPropUnused, + thisDestructPropsAliasDestructPropUnused, + thisDestructPropsAliasPropUnused, + thisDestructPropsDestructPropUnused, + thisPropsDestructPropUnused, + thisPropsPropUnused, + }; + + componentDidUpdate(prevProps) { + (prevProps as unknown).prevProp as unknown; + } + + shouldComponentUpdate(nextProps) { + (nextProps as unknown).nextProp as unknown; + } + + stateProps() { + ((this as unknown).setState as unknown)((_, props) => (props as unknown).setStateProp as unknown); + } + + thisPropsAlias() { + const props = (this as unknown).props as unknown; + + const { thisPropsAliasDestructProp } = props as unknown; + (props as unknown).thisPropsAliasProp as unknown; + } + + thisDestructPropsAlias() { + const { props } = this as unknown; + + const { thisDestructPropsAliasDestructProp } = props as unknown; + (props as unknown).thisDestructPropsAliasProp as unknown; + } + + render() { + const { props: { thisDestructPropsDestructProp } } = this as unknown; + const { thisPropsDestructProp } = (this as unknown).props as unknown; + ((this as unknown).props as unknown).thisPropsProp as unknown; + + return null; + } + } + `, + 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}
;', + message: '\'prevPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'nextPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'setStatePropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsAliasDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsAliasPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsAliasDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsAliasPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisDestructPropsDestructPropUnused\' PropType is defined but prop is never used' + }, { + message: '\'thisPropsDestructPropUnused\' PropType is defined but prop is never used' + }, { + 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, @@ -5554,6 +5876,19 @@ ruleTester.run('no-unused-prop-types', rule, { 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.BABEL_ESLINT, + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, parsers.TS([{ code: [ 'type Person = {', ' lastname: string', @@ -5568,6 +5903,21 @@ ruleTester.run('no-unused-prop-types', rule, { 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 = {', @@ -5583,6 +5933,21 @@ ruleTester.run('no-unused-prop-types', rule, { 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 = {', @@ -5598,6 +5963,21 @@ ruleTester.run('no-unused-prop-types', rule, { 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 = {', @@ -5614,6 +5994,22 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + 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 = {', @@ -5631,6 +6027,23 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + 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 {', @@ -5648,6 +6061,23 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + 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: [ 'interface Foo {', @@ -5665,6 +6095,23 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + code: [ + 'interface Foo {', + ' foo: string;', + ' [blah: string]: number;', + '}', + 'const Hello = ({bar}: Foo) => {', + ' return
Hello {bar}
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [ + { + message: '\'foo\' PropType is defined but prop is never used' + } + ] + }, { code: [ 'interface Props {', @@ -5681,6 +6128,22 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + code: [ + 'interface Props {', + ' \'aria-label\': string;', + '}', + 'export default function Component(props: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [ + { + message: '\'aria-label\' PropType is defined but prop is never used' + } + ] + }, { code: [ 'interface Props {', @@ -5697,6 +6160,22 @@ ruleTester.run('no-unused-prop-types', rule, { } ] }, + { + code: [ + 'interface Props {', + ' [1234]: string;', + '}', + 'export default function Component(props: Props): JSX.Element {', + ' return
;', + '}' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [ + { + message: '\'1234\' PropType is defined but prop is never used' + } + ] + }, { code: [ 'const Hello = ({firstname}: {firstname: string, lastname: string}) => {', @@ -5709,7 +6188,488 @@ ruleTester.run('no-unused-prop-types', rule, { 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' + } + ] + }, + { + // test same name of interface should be merge + code: ` + interface Foo { + x: number; + } + + interface Foo { + z: string; + } + + interface Bar extends Foo { + y: string; + } + + const Baz = ({ x, y }: Bar) => ( + + {x} + {y} + + );`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'z\' PropType is defined but prop is never used' + }] + }, + { + // test same name of interface should be merge + code: ` + interface Foo { + x: number; + } + + interface Foo { + z: string; + } + + interface Bar extends Foo { + y: string; + } + + const Baz = ({ x, y }: Bar) => ( + + {x} + {y} + + );`, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'z\' PropType is defined but prop is never used' + }] + }, + { + // test extends + code: ` + interface Foo { + x: number; + } + + interface Bar extends Foo { + y: string; + } + + const Baz = ({ x }: Bar) => ( + + {x} + + );`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'y\' PropType is defined but prop is never used' + }] + }, + { + // test extends + code: ` + interface Foo { + x: number; + } + + interface Bar { + y: string; + } + + interface Baz { + z:string; + } + + const Baz = ({ x }: Bar & Foo & Baz) => ( + + {x} + + );`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'y\' PropType is defined but prop is never used' + }, { + message: '\'z\' PropType is defined but prop is never used' + }] + }, + { + // test extends + code: ` + interface Foo { + x: number; + } + + interface Bar { + y: string; + } + + interface Baz { + z:string; + } + + const Baz = ({ x }: Bar & Foo & Baz) => ( + + {x} + + );`, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'y\' PropType is defined but prop is never used' + }, { + message: '\'z\' PropType is defined but prop is never used' + }] + }, + { + // test same name merge and extends + code: ` + interface Foo { + x: number; + } + + interface Foo { + z: string; + } + + interface Bar extends Foo { + y: string; + } + + const Baz = ({ x }: Bar) => ( + + {x} + + );`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'z\' PropType is defined but prop is never used' + }, {message: '\'y\' PropType is defined but prop is never used'}] + }, + { + // test same name merge and extends + code: ` + interface Foo { + x: number; + } + + interface Foo { + z: string; + } + + interface Bar extends Foo { + y: string; + } + + const Baz = ({ x }: Bar) => ( + + {x} + + );`, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'z\' PropType is defined but prop is never used' + }, {message: '\'y\' PropType is defined but prop is never used'}] + }, + { + // test same name merge and extends + code: ` + interface Foo { + x: number; + } + + interface Foo { + z: string; + } + + interface Foo { + y: string; + } + + const Baz = ({ x }: Foo) => ( + + {x} + + );`, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'z\' PropType is defined but prop is never used' + }, {message: '\'y\' PropType is defined but prop is never used'}] + }, + { + code: ` + type User = { + user: string; + } + + type UserProps = { + userId: string; + } + + type AgeProps = { + age: number; + } + + type BirthdayProps = { + birthday: string; + } + + type intersectionUserProps = AgeProps & BirthdayProps; + + type Props = User & UserProps & intersectionUserProps; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'age\' PropType is defined but prop is never used' + }, { + message: '\'birthday\' PropType is defined but prop is never used' + }] + }, + { + code: ` + type User = { + user: string; + } + + type UserProps = { + userId: string; + } + + type AgeProps = { + age: number; + } + + type BirthdayProps = { + birthday: string; + } + + type intersectionUserProps = AgeProps & BirthdayProps; + + type Props = User & UserProps & intersectionUserProps; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'age\' PropType is defined but prop is never used' + }, { + message: '\'birthday\' PropType is defined but prop is never used' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books + }); + + interface InfoLibTableProps extends ReturnType { + } + + const App = (props: InfoLibTableProps) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books + }); + + interface InfoLibTableProps extends ReturnType { + } + + const App = (props: InfoLibTableProps) => { + return
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books, + }); + + interface BooksTable extends ReturnType { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books, + }); + + interface BooksTable extends ReturnType { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + interface BooksTable extends ReturnType<() => {books:Array}> { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + interface BooksTable extends ReturnType<() => {books:Array}> { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + type BooksTable = ReturnType<() => {books:Array}> & { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + type BooksTable = ReturnType<() => {books:Array}> & { + username: string; + } + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + type mapStateToProps = ReturnType<() => {books:Array}>; + + type Props = { + username: string; + } + + type BooksTable = mapStateToProps & Props; + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }, + { + code: ` + type mapStateToProps = ReturnType<() => {books:Array}>; + + type Props = { + username: string; + } + + type BooksTable = mapStateToProps & Props; + + const App = (props: BooksTable) => { + return
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' PropType is defined but prop is never used' + }, { + message: '\'username\' PropType is defined but prop is never used' + }] + }]) /* , { // Enable this when the following issue is fixed @@ -5732,5 +6692,5 @@ ruleTester.run('no-unused-prop-types', rule, { message: '\'foo\' PropType is defined but prop is never used' }] } */ - ] + ) }); diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index f064d9a377..f4dbde1519 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -35,7 +35,7 @@ const settings = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('prop-types', rule, { - valid: [ + valid: [].concat( { code: [ 'var Hello = createReactClass({', @@ -2490,21 +2490,6 @@ ruleTester.run('prop-types', rule, { }; ` }, - { - code: ` - interface Props { - 'aria-label': string // 'undefined' PropType is defined but prop is never used eslint(react/no-unused-prop-types) - // 'undefined' PropType is defined but prop is never used eslint(react-redux/no-unused-prop-types) - } - - export default function Component({ - 'aria-label': ariaLabel, // 'aria-label' is missing in props validation eslint(react/prop-types) - }: Props): JSX.Element { - return
- } - `, - parser: parsers.TYPESCRIPT_ESLINT - }, // shouldn't trigger this rule since functions stating with a lowercase // letter are not considered components ` @@ -2512,94 +2497,412 @@ ruleTester.run('prop-types', rule, { return
{props.text}
} `, - // shouldn't trigger this rule for 'render' since functions stating with a lowercase - // letter are not considered components - ` - const MyComponent = (props) => { - const render = () => { - return {props.hello}; - } - return render(); - }; - MyComponent.propTypes = { - hello: PropTypes.string.isRequired, - }; - `, { code: ` - interface Props { - value?: string; - } + export default function() {} + ` + }, + parsers.TS([ + { + code: ` + interface Props { + 'aria-label': string // 'undefined' PropType is defined but prop is never used eslint(react/no-unused-prop-types) + // 'undefined' PropType is defined but prop is never used eslint(react-redux/no-unused-prop-types) + } - // without the | null, all ok, with it, it is broken - function Test ({ value }: Props): React.ReactElement | null { - if (!value) { - return null; - } + export default function Component({ + 'aria-label': ariaLabel, // 'aria-label' is missing in props validation eslint(react/prop-types) + }: Props): JSX.Element { + return
+ } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + interface Props { + value?: string; + } - return
{value}
; - }`, - parser: parsers.TYPESCRIPT_ESLINT - }, - { - code: ` - interface Props { - value?: string; - } + // without the | null, all ok, with it, it is broken + function Test ({ value }: Props): React.ReactElement | null { + if (!value) { + return null; + } - // without the | null, all ok, with it, it is broken - function Test ({ value }: Props): React.ReactElement | null { - if (!value) { - return
{value}
;; - } + return
{value}
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + interface Props { + value?: string; + } - return null; - }`, - parser: parsers.TYPESCRIPT_ESLINT - }, - { - code: ` - interface Props { - value?: string; - } - const Hello = (props: Props) => { - if (props.value) { - return
; - } - return null; - }`, - parser: parsers.TYPESCRIPT_ESLINT - }, - { - code: ` - export default function() {} + // without the | null, all ok, with it, it is broken + function Test ({ value }: Props): React.ReactElement | null { + if (!value) { + return
{value}
;; + } + + return null; + } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + interface Props { + value?: string; + } + const Hello = (props: Props) => { + if (props.value) { + return
; + } + return null; + } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + interface Props { + 'aria-label': string // 'undefined' PropType is defined but prop is never used eslint(react/no-unused-prop-types) + // 'undefined' PropType is defined but prop is never used eslint(react-redux/no-unused-prop-types) + } + + export default function Component({ + 'aria-label': ariaLabel, // 'aria-label' is missing in props validation eslint(react/prop-types) + }: Props): JSX.Element { + return
+ } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + // shouldn't trigger this rule for 'render' since functions stating with a lowercase + // letter are not considered components ` - }, - { - code: ` - import * as React from 'react'; + const MyComponent = (props) => { + const render = () => { + return {props.hello}; + } + return render(); + }; + MyComponent.propTypes = { + hello: PropTypes.string.isRequired, + }; + `, + { + code: ` + interface Props { + value?: string; + } - interface Props { - text: string; - } + // without the | null, all ok, with it, it is broken + function Test ({ value }: Props): React.ReactElement | null { + if (!value) { + return null; + } + return
{value}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface Props { + value?: string; + } - export const Test: React.FC = (props: Props) => { - const createElement = (text: string) => { - return ( -
- {text} -
- ); + // without the | null, all ok, with it, it is broken + function Test ({ value }: Props): React.ReactElement | null { + if (!value) { + return
{value}
;; + } + + return null; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface Props { + value?: string; + } + const Hello = (props: Props) => { + if (props.value) { + return
; + } + return null; + } + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + import * as React from 'react'; + + interface Props { + text: string; + } + + export const Test: React.FC = (props: Props) => { + const createElement = (text: string) => { + return ( +
+ {text} +
+ ); + }; + + return <>{createElement(props.text)}; }; + `, + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + interface Props { + value?: string; + } + const Hello = (props: Props) => { + if (props.value) { + return
; + } + return null; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books + }); - return <>{createElement(props.text)}; - }; - `, - parser: parsers.TYPESCRIPT_ESLINT - } - ], + interface InfoLibTableProps extends ReturnType { + } - invalid: [ + const App = (props: InfoLibTableProps) => { + props.books(); + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books + }); + + interface BooksTable extends ReturnType { + username: string; + } + + const App = (props: BooksTable) => { + props.books(); + return
{props.username}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface infoLibTable { + removeCollection(): Array; + } + + interface InfoLibTableProps extends ReturnType<(dispatch: storeDispatch) => infoLibTable> { + } + + const App = (props: InfoLibTableProps) => { + props.removeCollection(); + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface addTable { + createCollection: () => Array + } + + type infoLibTable = addTable & { + removeCollection: () => Array + } + + interface InfoLibTableProps extends ReturnType<(dispatch: storeDispatch) => infoLibTable> { + } + + const App = (props: InfoLibTableProps) => { + props.createCollection(); + props.removeCollection(); + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface InfoLibTableProps extends ReturnType<(dispatch: storeDispatch) => { + removeCollection: () => Array, + }> { + } + + const App = (props: InfoLibTableProps) => { + props.removeCollection(); + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface addTable { + createCollection: () => Array; + } + + type infoLibTable = { + removeCollection: () => Array, + }; + + interface InfoLibTableProps extends ReturnType< + (dispatch: storeDispatch) => infoLibTable & addTable, + >{} + + const App = (props: InfoLibTableProps) => { + props.createCollection(); + props.removeCollection(); + return
; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface addTable { + createCollection: () => Array; + } + + type infoLibTable = ReturnType<(dispatch: storeDispatch) => infoLibTable & addTable> & { + removeCollection: () => Array, + }; + + interface InfoLibTableProps {} + + const App = (props: infoLibTable) => { + props.createCollection(); + props.removeCollection(); + return
; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface InfoLibTableProps extends ReturnType<(dispatch: storeDispatch) => ({ + removeCollection: () => Array, + })> { + } + + const App = (props: InfoLibTableProps) => { + props.removeCollection(); + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface ThingProps extends React.HTMLAttributes { + thing?: number + } + + export const Thing = ({ thing = 1, style, ...props }: ThingProps) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface ThingProps { + thing?: number + } + + export const Thing = ({ thing = 1, style, ...props }: ThingProps & React.HTMLAttributes) => { + return
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type User = { + user: string; + } + + type Props = User & UserProps; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type User = { + } + + type Props = User & UserProps; + + export default (props: Props) => { + const { user } = props; + + if (user === 0) { + return

user is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface GenericProps { + onClose: () => void + } + + interface ImplementationProps extends GenericProps { + onClick: () => void + } + + export const Implementation: FC = ( + { + onClick, + onClose, + }: ImplementationProps + ) => (
) + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + } + ]) + ), + + invalid: [].concat( { code: [ 'type Props = {', @@ -5174,21 +5477,6 @@ ruleTester.run('prop-types', rule, { message: '\'foo.baz\' is missing in props validation' }] }, - { - code: ` - interface Props { - } - const Hello = (props: Props) => { - if (props.value) { - return
; - } - return null; - }`, - parser: parsers.TYPESCRIPT_ESLINT, - errors: [{ - message: '\'value\' is missing in props validation' - }] - }, { code: ` export default function ({ value = 'World' }) { @@ -5198,6 +5486,348 @@ ruleTester.run('prop-types', rule, { errors: [{ message: '\'value\' is missing in props validation' }] - } - ] + }, + parsers.TS([ + { + code: ` + interface Props { + } + const Hello = (props: Props) => { + if (props.value) { + return
; + } + return null; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'value\' is missing in props validation' + }] + }, + { + code: ` + interface Props { + } + const Hello = (props: Props) => { + if (props.value) { + return
; + } + return null; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'value\' is missing in props validation' + }] + }, + { + code: ` + type User = { + user: string; + } + + type Props = User & { + }; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'userId\' is missing in props validation' + }] + }, + { + code: ` + type User = { + user: string; + } + + type Props = User & { + }; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'userId\' is missing in props validation' + }] + }, + { + code: ` + type User = { + } + + type Props = User & { + userId + }; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'user\' is missing in props validation' + }] + }, + { + code: ` + type User = { + } + + type Props = User & { + userId + }; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'user\' is missing in props validation' + }] + }, + { + code: ` + type User = { + user: string; + } + type UserProps = { + } + + type Props = User & UserProps; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'userId\' is missing in props validation' + }] + }, + { + code: ` + type User = { + user: string; + } + type UserProps = { + } + + type Props = User & UserProps; + + export default (props: Props) => { + const { userId, user } = props; + + if (userId === 0) { + return

userId is 0

; + } + + return null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'userId\' is missing in props validation' + }] + }, + { + code: ` + interface GenericProps { + onClose: () => void + } + + interface ImplementationProps extends GenericProps { + } + + export const Implementation: FC = ( + { + onClick, + onClose, + }: ImplementationProps + ) => (
) + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'onClick\' is missing in props validation' + }] + }, + { + code: ` + interface GenericProps { + onClose: () => void + } + + interface ImplementationProps extends GenericProps { + } + + export const Implementation: FC = ( + { + onClick, + onClose, + }: ImplementationProps + ) => (
) + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'onClick\' is missing in props validation' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + }); + + interface BooksTable extends ReturnType { + username: string; + } + + const App = (props: BooksTable) => { + props.books(); + return
{props.username}
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'books\' is missing in props validation' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + }); + + interface BooksTable extends ReturnType { + username: string; + } + + const App = (props: BooksTable) => { + props.books(); + return
{props.username}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'books\' is missing in props validation' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books, + }); + + interface BooksTable extends ReturnType { + } + + const App = (props: BooksTable) => { + props.books(); + return
{props.username}
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'username\' is missing in props validation' + }] + }, + { + code: ` + const mapStateToProps = state => ({ + books: state.books, + }); + + interface BooksTable extends ReturnType { + } + + const App = (props: BooksTable) => { + props.books(); + return
{props.username}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'username\' is missing in props validation' + }] + }, + { + code: ` + type Event = { + name: string; + type: string; + } + + interface UserEvent extends Event { + UserId: string; + } + const App = (props: UserEvent) => { + props.name(); + props.type; + props.UserId; + return
{props.dateCreated}
; + } + `, + parser: parsers.TYPESCRIPT_ESLINT, + errors: [{ + message: '\'dateCreated\' is missing in props validation' + }] + }, + { + code: ` + type Event = { + name: string; + type: string; + } + + interface UserEvent extends Event { + UserId: string; + } + const App = (props: UserEvent) => { + props.name(); + props.type; + props.UserId; + return
{props.dateCreated}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: '\'dateCreated\' is missing in props validation' + }] + } + ]) + ) });