diff --git a/.eslintrc b/.eslintrc index 0667d9ddb5..bb628229e5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,7 @@ { "env": { - "node": true + "node": true, + "es6": true }, ecmaFeatures: { jsx: true diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 80befb599f..aeb2fb0a31 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -131,6 +131,45 @@ module.exports = { return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage; } + /** + * Checks whether the given identifier or member expression points to + * `Component`, `React.Component` or `React.PureComponent`. + * Doesn't support aliasing react to anything other than `React`. + * @param {ASTNode} node The AST node being checke.d + * @returns {Boolean} True if the node points to react, otherwise false. + */ + function isReactComponentClass(node) { + if (!node) { + return false; + } + if (node.type === 'Identifier' && node.name === 'Component') { + return true; + } else if (node.type === 'MemberExpression' && node.object.type === 'Identifier') { + return ( + node.object.name === 'React' && + node.property.type === 'Identifier' && + (node.property.name === 'Component' || node.property.name === 'PureComponent') + ); + } + return false; + } + + /** + * Checks if we are declaring a class which extends `React.Component` + * with `superTypeParameters`, e.g: + * `class Foo extends React.Component {}`. + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node is a type annotated props declaration, false if not. + */ + function isAnnotatedClass(node) { + if (node && (node.type === 'ClassExpression' || node.type === 'ClassDeclaration')) { + if (isReactComponentClass(node.superClass) && node.superTypeParameters) { + return true; + } + } + return false; + } + /** * Checks if we are declaring a `props` class property with a flow type annotation. * @param {ASTNode} node The AST node being checked. @@ -445,14 +484,25 @@ module.exports = { * Creates the representation of the React props type annotation for the component. * The representation is used to verify nested used properties. * @param {ASTNode} annotation Type annotation for the props class property. + * @param {Set} seen Keeps track of annotations we've already seen. * @return {Object|Boolean} The representation of the declaration, true means * the property is declared without the need for further analysis. */ - function buildTypeAnnotationDeclarationTypes(annotation) { + function buildTypeAnnotationDeclarationTypes(annotation, seen) { + if (seen === void 0) { + // Keeps track of annotations we've already seen to + // prevent problems with cyclic types. + seen = new Set(); + } + if (seen.has(annotation)) { + // this must be a recursive type annotation, just accept anything. + return true; + } + seen.add(annotation); switch (annotation.type) { case 'GenericTypeAnnotation': if (typeScope(annotation.id.name)) { - return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name)); + return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), seen); } return true; case 'ObjectTypeAnnotation': @@ -461,7 +511,7 @@ module.exports = { children: {} }; iterateProperties(annotation.properties, function(childKey, childValue) { - shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue); + shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue, seen); }); return shapeTypeDefinition; case 'UnionTypeAnnotation': @@ -470,7 +520,7 @@ module.exports = { children: [] }; for (var i = 0, j = annotation.types.length; i < j; i++) { - var type = buildTypeAnnotationDeclarationTypes(annotation.types[i]); + var type = buildTypeAnnotationDeclarationTypes(annotation.types[i], seen); // keep only complex type if (type !== true) { if (type.children === true) { @@ -491,7 +541,7 @@ module.exports = { return { type: 'object', children: { - __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType) + __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType, seen) } }; default: @@ -818,11 +868,28 @@ module.exports = { markAnnotatedFunctionArgumentsAsDeclared(node); } + /** + * Handles classes possibly annotated with Flow. + * @param {ASTNode} node We expect either a ClassDeclaration or ClassExpression. + */ + function handleClass(node) { + if (isAnnotatedClass(node)) { + var typeParameters = node.superTypeParameters.params; + if (typeParameters.length > 1) { + markPropTypesAsDeclared(node, resolveTypeAnnotation(typeParameters[1])); + } + } + } + // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { + ClassDeclaration: handleClass, + + ClassExpression: handleClass, + 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 c5161b24aa..5f6e35209b 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -927,6 +927,155 @@ ruleTester.run('prop-types', rule, { '}' ].join('\n'), parser: 'babel-eslint' + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Note = {text: string, children?: Note[]};', + 'type Props = {', + ' notes: Note[];', + '};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.notes[0].text}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {name: Object;};', + '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.name.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.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: [ + 'type Props = {name: {firstname: string; lastname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {name: {firstname: string;}};', + 'class Hello extends React.Component {', + ' render () {', + ' var names = [];', + ' for (var i = 0; i < this.props.people.length; i++) {', + ' names.push(this.props.people[i].name.firstname);', + ' }', + ' return
Hello {names.join(', ')}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {name: {firstname: string;}};', + 'type Props = {people: Person[];};', + 'class Hello extends React.Component {', + ' render () {', + ' var names = [];', + ' for (var i = 0; i < this.props.people.length; i++) {', + ' names.push(this.props.people[i].name.firstname);', + ' }', + ' return
Hello {names.join(', ')}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Person = {name: {firstname: string;}};', + 'type Props = {people: Person[]|Person;};', + 'class Hello extends React.Component {', + ' render () {', + ' var names = [];', + ' if (Array.isArray(this.props.people)) {', + ' for (var i = 0; i < this.props.people.length; i++) {', + ' names.push(this.props.people[i].name.firstname);', + ' }', + ' } else {', + ' names.push(this.props.people.name.firstname);', + ' }', + ' return
Hello {names.join(', ')}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'type Props = {ok: string | boolean;};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.ok}
;', + ' }', + '}' + ].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' + }, { + 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' }, { code: [ 'class Hello extends React.Component {', @@ -2110,6 +2259,141 @@ ruleTester.run('prop-types', rule, { errors: [ {message: '\'name\' is missing in props validation'} ] + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'firstname\' is missing in props validation'} + ] + }, { + code: [ + 'type Props = {name: Object;};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'firstname\' is missing in props validation'} + ] + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.lastname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'type Props = {name: {firstname: string;};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.name.lastname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.name.lastname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'person.name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'type Props = {person: {name: {firstname: string;};};};', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.name.lastname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'person.name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'type Person = {name: {firstname: string;}};', + 'class Hello extends React.Component {', + ' render () {', + ' var names = [];', + ' for (var i = 0; i < this.props.people.length; i++) {', + ' names.push(this.props.people[i].name.lastname);', + ' }', + ' return
Hello {names.join(', ')}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'people[].name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'type Person = {name: {firstname: string;}};', + 'type Props = {people: Person[];};', + 'class Hello extends React.Component {', + ' render () {', + ' var names = [];', + ' for (var i = 0; i < this.props.people.length; i++) {', + ' names.push(this.props.people[i].name.lastname);', + ' }', + ' return
Hello {names.join(', ')}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'people[].name.lastname\' is missing in props validation'} + ] + }, { + code: [ + 'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};', + 'class Hello extends React.PureComponent {', + ' render () {', + ' return
Hello {this.props.result.notok}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'result.notok\' is missing in props validation'} + ] + }, { + code: [ + 'class Hello extends Component {', + ' render () {', + ' return
Hello {this.props.name}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'name\' is missing in props validation'} + ] }, { code: [ 'class Hello extends React.Component {',