diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index af0e4f112b..6d0a19dab5 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -390,6 +390,27 @@ module.exports = function propTypesInstructions(context, components, utils) { }); } + /** + * Resolve node of type Identifier when building declaration types. + * @param {ASTNode} node + * @param {Function} callback called with the resolved value only if resolved. + */ + function resolveValueForIdentifierNode(node, callback) { + if ( + node + && node.type === 'Identifier' + ) { + const scope = context.getScope(); + const identVariable = scope.variableScope.variables.find( + (variable) => variable.name === node.name + ); + if (identVariable) { + const definition = identVariable.defs[identVariable.defs.length - 1]; + callback(definition.node.init); + } + } + } + /** * Creates the representation of the React propTypes for the component. * The representation is used to verify nested used properties. @@ -408,6 +429,23 @@ module.exports = function propTypesInstructions(context, components, utils) { return {}; } + let identNodeResolved = false; + // Resolve identifier node for cases where isRequired is set in + // the variable declaration or not at all. + // const variableType = PropTypes.shape({ foo: ... }).isRequired + // propTypes = { + // example: variableType + // } + // -------- + // const variableType = PropTypes.shape({ foo: ... }) + // propTypes = { + // example: variableType + // } + resolveValueForIdentifierNode(value, (newValue) => { + identNodeResolved = true; + value = newValue; + }); + if ( value && value.type === 'MemberExpression' @@ -418,6 +456,18 @@ module.exports = function propTypesInstructions(context, components, utils) { value = value.object; } + // Resolve identifier node for cases where isRequired is set in + // the prop types. + // const variableType = PropTypes.shape({ foo: ... }) + // propTypes = { + // example: variableType.isRequired + // } + if (!identNodeResolved) { + resolveValueForIdentifierNode(value, (newValue) => { + value = newValue; + }); + } + // Verify PropTypes that are functions if ( value diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index bf9926c382..316410f76b 100755 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -4949,6 +4949,138 @@ ruleTester.run('prop-types', rule, { { message: '\'ordering\' is missing in props validation' }] + }, + { + code: ` + const firstType = PropTypes.shape({ + id: PropTypes.number, + }); + class ComponentX extends React.Component { + static propTypes = { + first: firstType.isRequired, + }; + render() { + return ( +
+
Counter = {this.props.first.name}
+
+ ); + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: "'first.name' is missing in props validation" + }] + }, + { + code: ` + const firstType = PropTypes.shape({ + id: PropTypes.number, + }).isRequired; + class ComponentX extends React.Component { + static propTypes = { + first: firstType, + }; + render() { + return ( +
+
Counter = {this.props.first.name}
+
+ ); + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: "'first.name' is missing in props validation" + }] + }, + { + code: ` + const firstType = PropTypes.shape({ + id: PropTypes.number, + }); + class ComponentX extends React.Component { + static propTypes = { + first: firstType, + }; + render() { + return ( +
+
Counter = {this.props.first.name}
+
+ ); + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: "'first.name' is missing in props validation" + }] + }, + { + code: ` + function Foo({ + foo: { + bar: foo, + baz + }, + }) { + return

{foo.reduce(() => 5)}

; + } + const fooType = PropTypes.shape({ + bar: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired + Foo.propTypes = { + foo: fooType, + }; + `, + errors: [{ + message: '\'foo.baz\' is missing in props validation' + }] + }, + { + code: ` + function Foo({ + foo: { + bar: foo, + baz + }, + }) { + return

{foo.reduce(() => 5)}

; + } + const fooType = PropTypes.shape({ + bar: PropTypes.arrayOf(PropTypes.string).isRequired, + }) + Foo.propTypes = { + foo: fooType.isRequired, + }; + `, + errors: [{ + message: '\'foo.baz\' is missing in props validation' + }] + }, + { + code: ` + function Foo({ + foo: { + bar: foo, + baz + }, + }) { + return

{foo.reduce(() => 5)}

; + } + const fooType = PropTypes.shape({ + bar: PropTypes.arrayOf(PropTypes.string).isRequired, + }) + Foo.propTypes = { + foo: fooType, + }; + `, + errors: [{ + message: '\'foo.baz\' is missing in props validation' + }] } ] });