diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index d69f51e776..b03e05a392 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -359,7 +359,7 @@ module.exports = { value.callee.object && hasCustomValidator(value.callee.object.name) ) { - return true; + return {}; } if ( @@ -387,12 +387,12 @@ module.exports = { switch (callName) { case 'shape': if (skipShapeProps) { - return true; + return {}; } if (argument.type !== 'ObjectExpression') { // Invalid proptype or cannot analyse statically - return true; + return {}; } const shapeTypeDefinition = { type: 'shape', @@ -400,10 +400,7 @@ module.exports = { }; iterateProperties(argument.properties, (childKey, childValue) => { const fullName = [parentName, childKey].join('.'); - let types = buildReactDeclarationTypes(childValue, fullName); - if (types === true) { - types = {}; - } + const types = buildReactDeclarationTypes(childValue, fullName); types.fullName = fullName; types.name = childKey; types.node = childValue; @@ -413,10 +410,7 @@ module.exports = { case 'arrayOf': case 'objectOf': const fullName = [parentName, '*'].join('.'); - let child = buildReactDeclarationTypes(argument, fullName); - if (child === true) { - child = {}; - } + const child = buildReactDeclarationTypes(argument, fullName); child.fullName = fullName; child.name = '__ANY_KEY__'; child.node = argument; @@ -430,7 +424,7 @@ module.exports = { !argument.elements.length ) { // Invalid proptype or cannot analyse statically - return true; + return {}; } const unionTypeDefinition = { type: 'union', @@ -439,7 +433,7 @@ module.exports = { for (let i = 0, j = argument.elements.length; i < j; i++) { const type = buildReactDeclarationTypes(argument.elements[i], parentName); // keep only complex type - if (type !== true) { + if (Object.keys(type).length > 0) { if (type.children === true) { // every child is accepted for one type, abort type analysis unionTypeDefinition.children = true; @@ -451,7 +445,7 @@ module.exports = { } if (unionTypeDefinition.length === 0) { // no complex type found, simply accept everything - return true; + return {}; } return unionTypeDefinition; case 'instanceOf': @@ -462,11 +456,11 @@ module.exports = { }; case 'oneOf': default: - return true; + return {}; } } // Unknown property or accepts everything (any, object, ...) - return true; + return {}; } /** @@ -474,7 +468,7 @@ module.exports = { * The representation is used to verify nested used properties. * @param {ASTNode} annotation Type annotation for the props class property. * @param {String} parentName Name of the parent prop node. - * @return {Object|Boolean} The representation of the declaration, true means + * @return {Object} The representation of the declaration, an empty object means * the property is declared without the need for further analysis. */ function buildTypeAnnotationDeclarationTypes(annotation, parentName) { @@ -483,10 +477,10 @@ module.exports = { if (typeScope(annotation.id.name)) { return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), parentName); } - return true; + return {}; case 'ObjectTypeAnnotation': if (skipShapeProps) { - return true; + return {}; } const shapeTypeDefinition = { type: 'shape', @@ -494,10 +488,7 @@ module.exports = { }; iterateProperties(annotation.properties, (childKey, childValue) => { const fullName = [parentName, childKey].join('.'); - let types = buildTypeAnnotationDeclarationTypes(childValue, fullName); - if (types === true) { - types = {}; - } + const types = buildTypeAnnotationDeclarationTypes(childValue, fullName); types.fullName = fullName; types.name = childKey; types.node = childValue; @@ -512,7 +503,7 @@ module.exports = { for (let i = 0, j = annotation.types.length; i < j; i++) { const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName); // keep only complex type - if (type !== true) { + if (Object.keys(type).length > 0) { if (type.children === true) { // every child is accepted for one type, abort type analysis unionTypeDefinition.children = true; @@ -523,16 +514,13 @@ module.exports = { unionTypeDefinition.children.push(type); } if (unionTypeDefinition.children.length === 0) { - // no complex type found, simply accept everything - return true; + // no complex type found + return {}; } return unionTypeDefinition; case 'ArrayTypeAnnotation': const fullName = [parentName, '*'].join('.'); - let child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName); - if (child === true) { - child = {}; - } + const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName); child.fullName = fullName; child.name = '__ANY_KEY__'; child.node = annotation; @@ -542,7 +530,7 @@ module.exports = { }; default: // Unknown or accepts everything. - return true; + return {}; } } @@ -724,6 +712,60 @@ module.exports = { }); } + /** + * Marks all props found inside ObjectTypeAnnotaiton as declared. + * + * Modifies the declaredProperties object + * @param {ASTNode} propTypes + * @param {Object} declaredPropTypes + * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported) + */ + function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) { + let ignorePropsValidation = false; + + iterateProperties(propTypes.properties, (key, value) => { + if (!value) { + ignorePropsValidation = true; + return; + } + + const types = buildTypeAnnotationDeclarationTypes(value, key); + types.fullName = key; + types.name = key; + types.node = value; + declaredPropTypes.push(types); + }); + + return ignorePropsValidation; + } + + /** + * Marks all props found inside IntersectionTypeAnnotation as declared. + * Since InterSectionTypeAnnotations can be nested, this handles recursively. + * + * Modifies the declaredPropTypes object + * @param {ASTNode} propTypes + * @param {Object} declaredPropTypes + * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported) + */ + function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) { + return propTypes.types.some(annotation => { + if (annotation.type === 'ObjectTypeAnnotation') { + return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes); + } + + const typeNode = typeScope(annotation.id.name); + + if (!typeNode) { + return true; + } else if (typeNode.type === 'IntersectionTypeAnnotation') { + return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes); + } + + return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes); + }); + } + /** * Mark a prop type as declared * @param {ASTNode} node The AST node being checked. @@ -736,20 +778,7 @@ module.exports = { switch (propTypes && propTypes.type) { case 'ObjectTypeAnnotation': - iterateProperties(propTypes.properties, (key, value) => { - if (!value) { - ignorePropsValidation = true; - return; - } - let types = buildTypeAnnotationDeclarationTypes(value, key); - if (types === true) { - types = {}; - } - types.fullName = key; - types.name = key; - types.node = value; - declaredPropTypes.push(types); - }); + ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes); break; case 'ObjectExpression': iterateProperties(propTypes.properties, (key, value) => { @@ -757,10 +786,7 @@ module.exports = { ignorePropsValidation = true; return; } - let types = buildReactDeclarationTypes(value, key); - if (types === true) { - types = {}; - } + const types = buildReactDeclarationTypes(value, key); types.fullName = key; types.name = key; types.node = value; @@ -791,24 +817,7 @@ module.exports = { } break; case 'IntersectionTypeAnnotation': - propTypes.types.forEach(annotation => { - const propsType = typeScope(annotation.id.name); - iterateProperties(propsType.properties, (key, value) => { - if (!value) { - ignorePropsValidation = true; - return; - } - - let types = buildTypeAnnotationDeclarationTypes(value, key); - if (types === true) { - types = {}; - } - types.fullName = key; - types.name = key; - types.node = value; - declaredPropTypes.push(types); - }); - }); + ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes); break; case null: break; diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index c6f494afbf..aff7508634 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -249,6 +249,7 @@ module.exports = { function _isDeclaredInComponent(declaredPropTypes, keyList) { for (let i = 0, j = keyList.length; i < j; i++) { const key = keyList[i]; + const propType = ( declaredPropTypes && ( // Check if this key is declared @@ -261,7 +262,7 @@ module.exports = { // If it's a computed property, we can't make any further analysis, but is valid return key === '__COMPUTED_PROP__'; } - if (propType === true) { + if (typeof propType === 'object' && Object.keys(propType).length === 0) { return true; } // Consider every children as declared @@ -308,6 +309,7 @@ module.exports = { function isDeclaredInComponent(node, names) { while (node) { const component = components.get(node); + const isDeclared = component && component.confidence === 2 && _isDeclaredInComponent(component.declaredPropTypes || {}, names) @@ -377,7 +379,7 @@ module.exports = { * Creates the representation of the React propTypes for the component. * The representation is used to verify nested used properties. * @param {ASTNode} value Node of the PropTypes for the desired property - * @return {Object|Boolean} The representation of the declaration, true means + * @return {Object} The representation of the declaration, empty object means * the property is declared without the need for further analysis. */ function buildReactDeclarationTypes(value) { @@ -387,7 +389,7 @@ module.exports = { value.callee.object && hasCustomValidator(value.callee.object.name) ) { - return true; + return {}; } if ( @@ -416,7 +418,7 @@ module.exports = { case 'shape': if (argument.type !== 'ObjectExpression') { // Invalid proptype or cannot analyse statically - return true; + return {}; } const shapeTypeDefinition = { type: 'shape', @@ -440,7 +442,7 @@ module.exports = { !argument.elements.length ) { // Invalid proptype or cannot analyse statically - return true; + return {}; } const unionTypeDefinition = { type: 'union', @@ -449,7 +451,7 @@ module.exports = { for (let i = 0, j = argument.elements.length; i < j; i++) { const type = buildReactDeclarationTypes(argument.elements[i]); // keep only complex type - if (type !== true) { + if (Object.keys(type).length > 0) { if (type.children === true) { // every child is accepted for one type, abort type analysis unionTypeDefinition.children = true; @@ -461,7 +463,7 @@ module.exports = { } if (unionTypeDefinition.length === 0) { // no complex type found, simply accept everything - return true; + return {}; } return unionTypeDefinition; case 'instanceOf': @@ -472,18 +474,18 @@ module.exports = { }; case 'oneOf': default: - return true; + return {}; } } // Unknown property or accepts everything (any, object, ...) - return true; + return {}; } /** * 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. - * @return {Object|Boolean} The representation of the declaration, true means + * @return {Object} The representation of the declaration, empty object means * the property is declared without the need for further analysis. */ function buildTypeAnnotationDeclarationTypes(annotation) { @@ -492,7 +494,7 @@ module.exports = { if (typeScope(annotation.id.name)) { return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name)); } - return true; + return {}; case 'ObjectTypeAnnotation': let containsObjectTypeSpread = false; const shapeTypeDefinition = { @@ -509,7 +511,7 @@ module.exports = { // nested object type spread means we need to ignore/accept everything in this object if (containsObjectTypeSpread) { - return true; + return {}; } return shapeTypeDefinition; case 'UnionTypeAnnotation': @@ -520,7 +522,7 @@ module.exports = { for (let i = 0, j = annotation.types.length; i < j; i++) { const type = buildTypeAnnotationDeclarationTypes(annotation.types[i]); // keep only complex type - if (type !== true) { + if (Object.keys(type).length > 0) { if (type.children === true) { // every child is accepted for one type, abort type analysis unionTypeDefinition.children = true; @@ -532,7 +534,7 @@ module.exports = { } if (unionTypeDefinition.children.length === 0) { // no complex type found, simply accept everything - return true; + return {}; } return unionTypeDefinition; case 'ArrayTypeAnnotation': @@ -544,7 +546,7 @@ module.exports = { }; default: // Unknown or accepts everything. - return true; + return {}; } } @@ -710,6 +712,56 @@ module.exports = { }); } + /** + * Marks all props found inside ObjectTypeAnnotaiton as declared. + * + * Modifies the declaredProperties object + * @param {ASTNode} propTypes + * @param {Object} declaredPropTypes + * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported) + */ + function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) { + let ignorePropsValidation = false; + + iterateProperties(propTypes.properties, (key, value) => { + if (!value) { + ignorePropsValidation = true; + return; + } + + declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value); + }); + + return ignorePropsValidation; + } + + /** + * Marks all props found inside IntersectionTypeAnnotation as declared. + * Since InterSectionTypeAnnotations can be nested, this handles recursively. + * + * Modifies the declaredPropTypes object + * @param {ASTNode} propTypes + * @param {Object} declaredPropTypes + * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported) + */ + function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) { + return propTypes.types.some(annotation => { + if (annotation.type === 'ObjectTypeAnnotation') { + return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes); + } + + const typeNode = typeScope(annotation.id.name); + + if (!typeNode) { + return true; + } else if (typeNode.type === 'IntersectionTypeAnnotation') { + return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes); + } + + return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes); + }); + } + /** * Mark a prop type as declared * @param {ASTNode} node The AST node being checked. @@ -726,13 +778,7 @@ module.exports = { switch (propTypes && propTypes.type) { case 'ObjectTypeAnnotation': - iterateProperties(propTypes.properties, (key, value) => { - if (!value) { - ignorePropsValidation = true; - return; - } - declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value); - }); + ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes); break; case 'ObjectExpression': iterateProperties(propTypes.properties, (key, value) => { @@ -793,16 +839,7 @@ module.exports = { } break; case 'IntersectionTypeAnnotation': - propTypes.types.forEach(annotation => { - const propsType = typeScope(annotation.id.name); - iterateProperties(propsType.properties, (key, value) => { - if (!value) { - ignorePropsValidation = true; - return; - } - declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value); - }); - }); + ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, 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 0f9b204bc9..62ef6094c0 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -839,6 +839,72 @@ ruleTester.run('no-unused-prop-types', rule, { } `, parser: 'babel-eslint' + }, { + code: ` + type PropsA = { foo: string }; + type PropsB = { bar: string }; + type PropsC = { zap: string }; + type Props = PropsA & PropsB; + + class Bar extends React.Component { + props: Props & PropsC; + + render() { + return