diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 223ae0cb61..6ae5cb27aa 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -151,6 +151,21 @@ module.exports = { return false; } +/** + * 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 >= 2) { + return true; + } + } + return false; + } + /** * Checks if we are declaring a prop * @param {ASTNode} node The AST node being checked. @@ -803,6 +818,23 @@ module.exports = { return annotation; } + /** + * 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) { + var annotation = node.superTypeParameters.params[1]; + 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; + } + /** * @param {ASTNode} node We expect either an ArrowFunctionExpression, * FunctionDeclaration, or FunctionExpression @@ -839,6 +871,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/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index aed42e4568..7feadfb2ea 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -1414,6 +1414,88 @@ ruleTester.run('prop-types', rule, { '}' ].join('\n'), parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' const { firstname } = this.props;', + ' return
Hello {firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' const {', + ' firstname,', + ' } = this.props;', + ' return
Hello {firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {name: {firstname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'import type Props from "fake";', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {result?: {ok?: ?string | boolean;}|{ok?: ?number | Array}};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.result.ok}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' } ], @@ -2603,6 +2685,114 @@ ruleTester.run('prop-types', rule, { errors: [ {message: '\'name\' is missing in props validation'} ] + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'lastname\' is missing in props validation', + line: 6, + column: 35, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' const { lastname } = this.props;', + ' return
Hello {lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'lastname\' is missing in props validation', + line: 6, + column: 13, + type: 'Property' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' const {', + ' lastname,', + ' } = this.props;', + ' return
Hello {lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'lastname\' is missing in props validation', + line: 7, + column: 7, + type: 'Property' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {name: {firstname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'name.lastname\' is missing in props validation', + line: 4, + column: 40, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.result.notok}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'result.notok\' is missing in props validation', + line: 4, + column: 42, + type: 'Identifier' + }], + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {', + ' firstname: string', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.lastname}
;', + ' }', + '}' + ].join('\n'), + errors: [{ + message: '\'person.lastname\' is missing in props validation', + line: 6, + column: 42, + type: 'Identifier' + }], + parser: 'babel-eslint' } ] });