diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index 64de1fbd25..3948d67398 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -146,7 +146,7 @@ module.exports = { function mustBeValidated(component) { return Boolean( component && - !component.ignorePropsValidation + !component.ignoreUnusedPropTypesValidation ); } @@ -397,7 +397,7 @@ module.exports = { const component = components.get(utils.getParentComponent()); const usedPropTypes = component && component.usedPropTypes || []; - let ignorePropsValidation = component && component.ignorePropsValidation || false; + let ignoreUnusedPropTypesValidation = component && component.ignoreUnusedPropTypesValidation || false; switch (type) { case 'direct': @@ -414,7 +414,7 @@ module.exports = { case 'destructuring': for (let k = 0, l = (properties || []).length; k < l; k++) { if (hasSpreadOperator(properties[k]) || properties[k].computed) { - ignorePropsValidation = true; + ignoreUnusedPropTypesValidation = true; break; } const propName = getKeyValue(properties[k]); @@ -441,7 +441,7 @@ module.exports = { components.set(component ? component.node : node, { usedPropTypes: usedPropTypes, - ignorePropsValidation: ignorePropsValidation + ignoreUnusedPropTypesValidation: ignoreUnusedPropTypesValidation }); } diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index 239c58bcec..c35e811bb4 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -191,7 +191,7 @@ module.exports = { return true; } // Consider every children as declared - if (propType.children === true) { + if (propType.children === true || propType.containsSpread) { return true; } if (propType.acceptedProperties) { diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 88be549151..d58af934de 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -143,10 +143,10 @@ module.exports = function propTypesInstructions(context, components, utils) { } }); - // nested object type spread means we need to ignore/accept everything in this object - if (containsObjectTypeSpread) { - return {}; - } + // Mark if this shape has spread. 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; + return shapeTypeDefinition; }, @@ -669,7 +669,7 @@ module.exports = function propTypesInstructions(context, components, utils) { JSXSpreadAttribute: function(node) { const component = components.get(utils.getParentComponent()); components.set(component ? component.node : node, { - ignorePropsValidation: true + ignoreUnusedPropTypesValidation: true }); }, diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 61bff0fee9..e29b4888eb 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -1801,34 +1801,6 @@ ruleTester.run('no-unused-prop-types', rule, { ' bar: PropTypes.bool', '};' ].join('\n') - }, { - code: [ - 'type Person = {', - ' ...data,', - ' lastname: string', - '};', - 'class Hello extends React.Component {', - ' props: Person;', - ' render () {', - ' return
Hello {this.props.firstname}
;', - ' }', - '}' - ].join('\n'), - parser: 'babel-eslint' - }, { - code: [ - 'type Person = {|', - ' ...data,', - ' lastname: string', - '|};', - 'class Hello extends React.Component {', - ' props: Person;', - ' render () {', - ' return
Hello {this.props.firstname}
;', - ' }', - '}' - ].join('\n'), - parser: 'babel-eslint' }, { // The next two test cases are related to: https://github.com/yannickcr/eslint-plugin-react/issues/1183 code: [ @@ -2470,6 +2442,20 @@ ruleTester.run('no-unused-prop-types', rule, { '}' ].join('\n'), parser: 'babel-eslint' + }, { + code: [ + 'const foo = {};', + 'class Hello extends React.Component {', + ' render() {', + ' const {firstname, lastname} = this.props.name;', + ' return
{firstname} {lastname}
;', + ' }', + '}', + 'Hello.propTypes = {', + ' name: PropTypes.shape(foo)', + '};' + ].join('\n'), + parser: 'babel-eslint' }, { // issue #933 code: [ @@ -2913,6 +2899,38 @@ ruleTester.run('no-unused-prop-types', rule, { } `, parser: 'babel-eslint' + }, { + code: [ + 'import type {BasePerson} from \'./types\'', + 'type Props = {', + ' person: {', + ' ...$Exact,', + ' lastname: string', + ' }', + '};', + 'class Hello extends React.Component {', + ' props: Props;', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint' + }, { + code: [ + 'import BasePerson from \'./types\'', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}', + 'Hello.propTypes = {', + ' person: ProTypes.shape({', + ' ...BasePerson,', + ' lastname: PropTypes.string', + ' })', + '};' + ].join('\n') } ], @@ -4475,6 +4493,48 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [{ message: '\'lastname\' PropType is defined but prop is never used' }] + }, { + code: ` + type Person = string; + class Hello extends React.Component<{ person: Person }> { + render () { + return
; + } + } + `, + settings: {react: {flowVersion: '0.53'}}, + errors: [{ + message: '\'person\' PropType is defined but prop is never used' + }], + parser: 'babel-eslint' + }, { + code: ` + type Person = string; + class Hello extends React.Component { + render () { + return
; + } + } + `, + settings: {react: {flowVersion: '0.52'}}, + errors: [{ + message: '\'person\' PropType is defined but prop is never used' + }], + parser: 'babel-eslint' + }, { + code: ` + function higherOrderComponent() { + return class extends React.Component

{ + render() { + return

; + } + } + } + `, + errors: [{ + message: '\'foo\' PropType is defined but prop is never used' + }], + parser: 'babel-eslint' }, { // issue #1506 code: [ @@ -4665,6 +4725,198 @@ ruleTester.run('no-unused-prop-types', rule, { }, { message: '\'a.b.c\' PropType is defined but prop is never used' }] + }, { + code: ` + type Props = { foo: string } + function higherOrderComponent() { + return class extends React.Component { + render() { + return
; + } + } + } + `, + parser: 'babel-eslint', + errors: [{ + message: '\'foo\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {', + ' ...data,', + ' lastname: string', + '};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {|', + ' ...data,', + ' lastname: string', + '|};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {', + ' ...$Exact,', + ' lastname: string', + '};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'import type {Data} from \'./Data\'', + 'type Person = {', + ' ...Data,', + ' lastname: string', + '};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.bar}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'import type {Data} from \'some-libdef-like-flow-typed-provides\'', + 'type Person = {', + ' ...Data,', + ' lastname: string', + '};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.bar}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {', + ' ...data,', + ' lastname: string', + '};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'type Person = {|', + ' ...data,', + ' lastname: string', + '|};', + 'class Hello extends React.Component {', + ' props: Person;', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.firstname}
;', + ' }', + '}', + 'Hello.propTypes = {', + ' ...BasePerson,', + ' lastname: PropTypes.string', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: '\'lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'import type {BasePerson} from \'./types\'', + 'type Props = {', + ' person: {', + ' ...$Exact,', + ' lastname: string', + ' }', + '};', + 'class Hello extends React.Component {', + ' props: Props;', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}' + ].join('\n'), + parser: 'babel-eslint', + options: [{skipShapeProps: false}], + errors: [{ + message: '\'person.lastname\' PropType is defined but prop is never used' + }] + }, { + code: [ + 'import BasePerson from \'./types\'', + 'class Hello extends React.Component {', + ' render () {', + ' return
Hello {this.props.person.firstname}
;', + ' }', + '}', + 'Hello.propTypes = {', + ' person: ProTypes.shape({', + ' ...BasePerson,', + ' lastname: PropTypes.string', + ' })', + '};' + ].join('\n'), + options: [{skipShapeProps: false}], + errors: [{ + message: '\'person.lastname\' PropType is defined but prop is never used' + }] } /* , { diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index a879f05434..bdc492f262 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -342,6 +342,51 @@ ruleTester.run('prop-types', rule, { ' ])', '};' ].join('\n') + }, { + code: ` + class Component extends React.Component { + render() { + return
{this.props.foo.baz}
; + } + } + Component.propTypes = { + foo: PropTypes.oneOfType([ + PropTypes.shape({ + bar: PropTypes.string + }), + PropTypes.shape({ + baz: PropTypes.string + }) + ]) + }; + ` + }, { + code: ` + class Component extends React.Component { + render() { + return
{this.props.foo.baz}
; + } + } + Component.propTypes = { + foo: PropTypes.oneOfType([ + PropTypes.shape({ + bar: PropTypes.string + }), + PropTypes.instanceOf(Baz) + ]) + }; + ` + }, { + code: ` + class Component extends React.Component { + render() { + return
{this.props.foo.baz}
; + } + } + Component.propTypes = { + foo: PropTypes.oneOf(['bar', 'baz']) + }; + ` }, { code: [ 'class Hello extends React.Component {', @@ -478,6 +523,20 @@ ruleTester.run('prop-types', rule, { '};' ].join('\n'), parser: 'babel-eslint' + }, { + code: [ + 'const foo = {};', + 'class Hello extends React.Component {', + ' render() {', + ' const {firstname, lastname} = this.props.name;', + ' return
{firstname} {lastname}
;', + ' }', + '}', + 'Hello.propTypes = {', + ' name: PropTypes.shape(foo)', + '};' + ].join('\n'), + parser: 'babel-eslint' }, { code: [ 'class Hello extends React.Component {', @@ -1209,6 +1268,20 @@ ruleTester.run('prop-types', rule, { '} & FieldProps' ].join('\n'), parser: 'babel-eslint' + }, { + // Impossible intersection type + code: ` + import React from 'react'; + type Props = string & { + fullname: string + }; + class Test extends React.PureComponent { + render() { + return
Hello {this.props.fullname}
+ } + } + `, + parser: 'babel-eslint' }, { code: [ 'Card.propTypes = {', @@ -3760,6 +3833,25 @@ ruleTester.run('prop-types', rule, { message: '\'bad\' is missing in props validation' }], parser: 'babel-eslint' + }, + { + code: ` + class Component extends React.Component { + render() { + return
{this.props.foo.baz}
; + } + } + Component.propTypes = { + foo: PropTypes.oneOfType([ + PropTypes.shape({ + bar: PropTypes.string + }) + ]) + }; + `, + errors: [{ + message: '\'foo.baz\' is missing in props validation' + }] } ] });