diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index c652e5bc12..aa53691567 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -103,7 +103,7 @@ module.exports = { return true; } // Consider every children as declared - if (propType.children === true || propType.containsSpread || propType.containsIndexers) { + if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) { return true; } if (propType.acceptedProperties) { diff --git a/lib/util/ast.js b/lib/util/ast.js index 13d9ff8997..1111c992d9 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -165,6 +165,9 @@ function getKeyValue(context, node) { stripQuotes(tokens[0].value) ); } + if (node.type === 'GenericTypeAnnotation') { + return node.id.name; + } const key = node.key || node.argument; return key.type === 'Identifier' ? key.name : key.value; } diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index b03607b73c..c60f92c767 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -32,13 +32,19 @@ function isSuperTypeParameterPropsDeclaration(node) { * @param {Object[]} properties Array of properties to iterate. * @param {Function} fn Function to call on each property, receives property key and property value. (key, value) => void + * @param {Function} [handleSpreadFn] Function to call on each ObjectTypeSpreadProperty, receives the + argument */ -function iterateProperties(context, properties, fn) { +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); + if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') { + handleSpreadFn(node.argument); + } + const value = node.value; fn(key, value, node); } @@ -121,7 +127,8 @@ module.exports = function propTypesInstructions(context, components, utils) { }, ObjectTypeAnnotation(annotation, parentName, seen) { - let containsObjectTypeSpread = false; + let containsUnresolvedObjectTypeSpread = false; + let containsSpread = false; const containsIndexers = Boolean(annotation.indexers && annotation.indexers.length); const shapeTypeDefinition = { type: 'shape', @@ -129,9 +136,7 @@ module.exports = function propTypesInstructions(context, components, utils) { }; iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => { const fullName = [parentName, childKey].join('.'); - if (!childKey && !childValue) { - containsObjectTypeSpread = true; - } else { + if (childKey || childValue) { const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen); types.fullName = fullName; types.name = childKey; @@ -139,12 +144,24 @@ module.exports = function propTypesInstructions(context, components, utils) { types.isRequired = !childValue.optional; shapeTypeDefinition.children[childKey] = types; } + }, + (spreadNode) => { + const key = getKeyValue(context, spreadNode); + const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen); + if (!types.children) { + containsUnresolvedObjectTypeSpread = true; + } else { + Object.assign(shapeTypeDefinition, types.children); + } + containsSpread = true; }); // Mark if this shape has spread or an indexer. We will know to consider all props from this shape as having propTypes, // but still have the ability to detect unused children of this shape. - shapeTypeDefinition.containsSpread = containsObjectTypeSpread; + shapeTypeDefinition.containsUnresolvedSpread = containsUnresolvedObjectTypeSpread; shapeTypeDefinition.containsIndexers = containsIndexers; + // Deprecated: containsSpread is not used anymore in the codebase, ensure to keep API backward compatibility + shapeTypeDefinition.containsSpread = containsSpread; return shapeTypeDefinition; }, @@ -241,7 +258,7 @@ module.exports = function propTypesInstructions(context, components, utils) { } /** - * Marks all props found inside ObjectTypeAnnotaiton as declared. + * Marks all props found inside ObjectTypeAnnotation as declared. * * Modifies the declaredProperties object * @param {ASTNode} propTypes @@ -253,7 +270,7 @@ module.exports = function propTypesInstructions(context, components, utils) { iterateProperties(context, propTypes.properties, (key, value, propNode) => { if (!value) { - ignorePropsValidation = true; + ignorePropsValidation = ignorePropsValidation || propNode.type !== 'ObjectTypeSpreadProperty'; return; } @@ -263,6 +280,15 @@ module.exports = function propTypesInstructions(context, components, utils) { types.node = propNode; types.isRequired = !propNode.optional; declaredPropTypes[key] = types; + }, (spreadNode) => { + const key = getKeyValue(context, spreadNode); + const spreadAnnotation = getInTypeScope(key); + if (!spreadAnnotation) { + ignorePropsValidation = true; + } else { + const spreadIgnoreValidation = declarePropTypesForObjectTypeAnnotation(spreadAnnotation, declaredPropTypes); + ignorePropsValidation = ignorePropsValidation || spreadIgnoreValidation; + } }); return ignorePropsValidation; diff --git a/tests/lib/rules/default-props-match-prop-types.js b/tests/lib/rules/default-props-match-prop-types.js index 0e6c6d0be6..5e244cbec7 100644 --- a/tests/lib/rules/default-props-match-prop-types.js +++ b/tests/lib/rules/default-props-match-prop-types.js @@ -733,6 +733,58 @@ ruleTester.run('default-props-match-prop-types', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT }, + { + code: [ + 'type DefaultProps1 = {|', + ' bar1?: string', + '|};', + 'type DefaultProps2 = {|', + ' ...DefaultProps1,', + ' bar2?: string', + '|};', + 'type Props = {', + ' foo: string,', + ' ...DefaultProps2', + '};', + + 'function Hello(props: Props) {', + ' return