From fb5c87f4db13d1bab2df43c1376474d8ead3beb4 Mon Sep 17 00:00:00 2001 From: Alex Zherdev Date: Sat, 4 Aug 2018 23:52:46 -0700 Subject: [PATCH 1/3] Additional tests for prop-types and no-unused-prop-types --- lib/rules/no-unused-prop-types.js | 8 +- lib/rules/prop-types.js | 2 +- lib/util/propTypes.js | 10 +- tests/lib/rules/no-unused-prop-types.js | 258 +++++++++++++++++++++--- tests/lib/rules/prop-types.js | 92 +++++++++ 5 files changed, 332 insertions(+), 38 deletions(-) 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..afa4b5a94b 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,23 @@ 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', } ], @@ -4475,6 +4478,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 +4710,163 @@ 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: [ + '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' + }] } /* , { 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' + }] } ] }); From e5bf7da85a25eed225485be2845c03c4c5a28f98 Mon Sep 17 00:00:00 2001 From: Alex Zherdev Date: Tue, 14 Aug 2018 23:47:31 -0700 Subject: [PATCH 2/3] Fix lint --- tests/lib/rules/no-unused-prop-types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index afa4b5a94b..6f6012dfc2 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -2915,7 +2915,7 @@ ruleTester.run('no-unused-prop-types', rule, { ' }', '}' ].join('\n'), - parser: 'babel-eslint', + parser: 'babel-eslint' } ], From 2d285120dbcb4e9002ebf74a97cf4b3c437ee63c Mon Sep 17 00:00:00 2001 From: Alex Zherdev Date: Wed, 15 Aug 2018 10:21:59 -0700 Subject: [PATCH 3/3] Add propTypes tests --- tests/lib/rules/no-unused-prop-types.js | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 6f6012dfc2..e29b4888eb 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -2916,6 +2916,21 @@ ruleTester.run('no-unused-prop-types', rule, { '}' ].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') } ], @@ -4846,6 +4861,22 @@ ruleTester.run('no-unused-prop-types', rule, { 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\'', @@ -4867,6 +4898,25 @@ ruleTester.run('no-unused-prop-types', rule, { 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' + }] } /* , {