diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index d076368777..2640ae352d 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -11,6 +11,7 @@ const has = require('has'); const Components = require('../util/Components'); const variable = require('../util/variable'); const annotations = require('../util/annotations'); +const versionUtil = require('../util/version'); // ------------------------------------------------------------------------------ // Constants @@ -133,6 +134,48 @@ module.exports = { return false; } + /** + * Resolve the type annotation for a given class declaration node with superTypeParameters. + * + * @param {ASTNode} node The annotation or a node containing the type annotation. + * @returns {ASTNode} The resolved type annotation for the node. + */ + function resolveSuperParameterPropsType(node) { + let propsParameterPosition; + try { + // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props. + // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props. + propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1; + } catch (e) { + // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52 + propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1; + } + + let annotation = node.superTypeParameters.params[propsParameterPosition]; + while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) { + annotation = annotation.typeAnnotation; + } + if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) { + return typeScope(annotation.id.name); + } + return annotation; + } + + /** + * Checks if we are declaring a props as a generic type in a flow-annotated class. + * + * @param {ASTNode} node the AST node being checked. + * @returns {Boolean} True if the node is a class with generic prop types, false if not. + */ + function isSuperTypeParameterPropsDeclaration(node) { + if (node && node.type === 'ClassDeclaration') { + if (node.superTypeParameters && node.superTypeParameters.params.length > 0) { + return true; + } + } + return false; + } + /** * Checks if we are declaring a prop * @param {ASTNode} node The AST node being checked. @@ -866,6 +909,12 @@ module.exports = { // -------------------------------------------------------------------------- return { + ClassDeclaration: function(node) { + if (isSuperTypeParameterPropsDeclaration(node)) { + markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node)); + } + }, + ClassProperty: function(node) { if (isAnnotatedClassPropsDeclaration(node)) { markPropTypesAsDeclared(node, resolveTypeAnnotation(node)); diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 4a9d1d6c6f..88e3121ee5 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -12,6 +12,7 @@ const Components = require('../util/Components'); const variable = require('../util/variable'); const annotations = require('../util/annotations'); const versionUtil = require('../util/version'); + // ------------------------------------------------------------------------------ // Constants // ------------------------------------------------------------------------------ diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 3985e4cca1..2a1897426f 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -2069,6 +2069,56 @@ ruleTester.run('no-unused-prop-types', rule, { }; `, parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + parser: 'babel-eslint' + }, { + code: ` + type Person = { + firstname: string + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, + parser: 'babel-eslint' } ], @@ -3469,6 +3519,72 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [{ message: '\'aProp\' PropType is defined but prop is never used' }] + }, { + code: ` + type Props = { + firstname: string, + lastname: string, + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: ` + type Props = { + firstname: string, + lastname: string, + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + settings: {react: {flowVersion: '0.52'}}, + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: ` + type Props = { + firstname: string, + lastname: string, + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: ` + type Props = { + firstname: string, + lastname: string, + } + class MyComponent extends React.Component { + render() { + return
Hello {this.props.firstname}
+ } + } + `, + settings: {react: {flowVersion: '0.53'}}, + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] } /* , {