diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js
index 590378e8a3..64de1fbd25 100644
--- a/lib/rules/no-unused-prop-types.js
+++ b/lib/rules/no-unused-prop-types.js
@@ -9,11 +9,8 @@
const has = require('has');
const Components = require('../util/Components');
-const variable = require('../util/variable');
-const annotations = require('../util/annotations');
const astUtil = require('../util/ast');
const versionUtil = require('../util/version');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
@@ -57,36 +54,12 @@ module.exports = {
},
create: Components.detect((context, components, utils) => {
- const defaults = {skipShapeProps: true};
const sourceCode = context.getSourceCode();
- const configuration = Object.assign({}, defaults, context.options[0] || {});
- const skipShapeProps = configuration.skipShapeProps;
- const customValidators = configuration.customValidators || [];
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0');
-
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
-
+ const defaults = {skipShapeProps: true, customValidators: []};
+ const configuration = Object.assign({}, defaults, context.options[0] || {});
const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used';
- /**
- * Helper for accessing the current scope in the stack.
- * @param {string} key The name of the identifier to access. If omitted, returns the full scope.
- * @param {ASTNode} value If provided sets the new value for the identifier.
- * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
- */
- function typeScope(key, value) {
- if (arguments.length === 0) {
- return stack[stack.length - 1];
- } else if (arguments.length === 1) {
- return stack[stack.length - 1][key];
- }
- stack[stack.length - 1][key] = value;
- return value;
- }
-
/**
* Check if we are in a lifecycle method
* @return {boolean} true if we are in a class constructor, false if not
@@ -165,77 +138,6 @@ module.exports = {
return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod();
}
- /**
- * Checks if we are declaring a `props` class property with a flow type annotation.
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if the node is a type annotated props declaration, false if not.
- */
- function isAnnotatedClassPropsDeclaration(node) {
- if (node && node.type === 'ClassProperty') {
- const tokens = context.getFirstTokens(node, 2);
- if (
- node.typeAnnotation && (
- tokens[0].value === 'props' ||
- (tokens[1] && tokens[1].value === 'props')
- )
- ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Resolve the type annotation for a given class declaration node with superTypeParameters.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveSuperParameterPropsType(node) {
- let propsParameterPosition;
- try {
- // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
- // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
- propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
- } catch (e) {
- // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
- propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
- }
-
- let annotation = node.superTypeParameters.params[propsParameterPosition];
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * Checks if we are declaring a props as a generic type in a flow-annotated class.
- *
- * @param {ASTNode} node the AST node being checked.
- * @returns {Boolean} True if the node is a class with generic prop types, false if not.
- */
- function isSuperTypeParameterPropsDeclaration(node) {
- if (node && node.type === 'ClassDeclaration') {
- if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if prop should be validated by plugin-react-proptypes
- * @param {String} validator Name of validator to check.
- * @returns {Boolean} True if validator should be checked by custom validator.
- */
- function hasCustomValidator(validator) {
- return customValidators.indexOf(validator) !== -1;
- }
-
/**
* Checks if the component must be validated
* @param {Object} component The component to process
@@ -355,214 +257,6 @@ module.exports = {
return key.type === 'Identifier' ? key.name : key.value;
}
- /**
- * Iterates through a properties node, like a customized forEach.
- * @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
- */
- function iterateProperties(properties, fn) {
- if (properties && properties.length && typeof fn === 'function') {
- for (let i = 0, j = properties.length; i < j; i++) {
- const node = properties[i];
- const key = getKeyValue(node);
-
- const value = node.value;
- fn(key, value);
- }
- }
- }
-
- /**
- * 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
- * @param {String} parentName Name of the parent prop node.
- * @return {Object|Boolean} The representation of the declaration, true means
- * the property is declared without the need for further analysis.
- */
- function buildReactDeclarationTypes(value, parentName) {
- if (
- value &&
- value.callee &&
- value.callee.object &&
- hasCustomValidator(value.callee.object.name)
- ) {
- return {};
- }
-
- if (
- value &&
- value.type === 'MemberExpression' &&
- value.property &&
- value.property.name &&
- value.property.name === 'isRequired'
- ) {
- value = value.object;
- }
-
- // Verify PropTypes that are functions
- if (
- value &&
- value.type === 'CallExpression' &&
- value.callee &&
- value.callee.property &&
- value.callee.property.name &&
- value.arguments &&
- value.arguments.length > 0
- ) {
- const callName = value.callee.property.name;
- const argument = value.arguments[0];
- switch (callName) {
- case 'shape':
- if (skipShapeProps) {
- return {};
- }
-
- if (argument.type !== 'ObjectExpression') {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: []
- };
- iterateProperties(argument.properties, (childKey, childValue) => {
- const fullName = [parentName, childKey].join('.');
- const types = buildReactDeclarationTypes(childValue, fullName);
- types.fullName = fullName;
- types.name = childKey;
- types.node = childValue;
- shapeTypeDefinition.children.push(types);
- });
- return shapeTypeDefinition;
- case 'arrayOf':
- case 'objectOf':
- const fullName = [parentName, '*'].join('.');
- const child = buildReactDeclarationTypes(argument, fullName);
- child.fullName = fullName;
- child.name = '__ANY_KEY__';
- child.node = argument;
- return {
- type: 'object',
- children: [child]
- };
- case 'oneOfType':
- if (
- !argument.elements ||
- !argument.elements.length
- ) {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = argument.elements.length; i < j; i++) {
- const type = buildReactDeclarationTypes(argument.elements[i], parentName);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'instanceOf':
- return {
- type: 'instance',
- // Accept all children because we can't know what type they are
- children: true
- };
- case 'oneOf':
- default:
- return {};
- }
- }
- // Unknown property or accepts everything (any, object, ...)
- 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.
- * @param {String} parentName Name of the parent prop node.
- * @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) {
- switch (annotation.type) {
- case 'GenericTypeAnnotation':
- if (typeScope(annotation.id.name)) {
- return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), parentName);
- }
- return {};
- case 'ObjectTypeAnnotation':
- if (skipShapeProps) {
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: []
- };
- iterateProperties(annotation.properties, (childKey, childValue) => {
- const fullName = [parentName, childKey].join('.');
- const types = buildTypeAnnotationDeclarationTypes(childValue, fullName);
- types.fullName = fullName;
- types.name = childKey;
- types.node = childValue;
- shapeTypeDefinition.children.push(types);
- });
- return shapeTypeDefinition;
- case 'UnionTypeAnnotation':
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = annotation.types.length; i < j; i++) {
- const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.children.length === 0) {
- // no complex type found
- return {};
- }
- return unionTypeDefinition;
- case 'ArrayTypeAnnotation':
- const fullName = [parentName, '*'].join('.');
- const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName);
- child.fullName = fullName;
- child.name = '__ANY_KEY__';
- child.node = annotation;
- return {
- type: 'object',
- children: [child]
- };
- default:
- // Unknown or accepts everything.
- return {};
- }
- }
-
/**
* Check if we are in a class constructor
* @return {boolean} true if we are in a class constructor, false if not
@@ -751,135 +445,6 @@ 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);
- }
-
- // Type can't be resolved
- if (!annotation.id) {
- return true;
- }
-
- 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.
- * @param {propTypes} node The AST node containing the proptypes
- */
- function markPropTypesAsDeclared(node, propTypes) {
- const component = components.get(node);
- const declaredPropTypes = component && component.declaredPropTypes || [];
- let ignorePropsValidation = component && component.ignorePropsValidation || false;
-
- switch (propTypes && propTypes.type) {
- case 'ObjectTypeAnnotation':
- ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case 'ObjectExpression':
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
- const types = buildReactDeclarationTypes(value, key);
- types.fullName = key;
- types.name = key;
- types.node = value;
- declaredPropTypes.push(types);
- // Handle custom prop validators using props inside
- if (astUtil.isFunctionLikeExpression(value)) {
- markPropTypesAsUsed(value);
- }
- });
- break;
- case 'MemberExpression':
- break;
- case 'Identifier':
- const variablesInScope = variable.variablesInScope(context);
- for (let i = 0, j = variablesInScope.length; i < j; i++) {
- if (variablesInScope[i].name !== propTypes.name) {
- continue;
- }
- const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
- markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
- return;
- }
- ignorePropsValidation = true;
- break;
- case 'CallExpression':
- if (
- propWrapperFunctions.has(propTypes.callee.name) &&
- propTypes.arguments && propTypes.arguments[0]
- ) {
- markPropTypesAsDeclared(node, propTypes.arguments[0]);
- return;
- }
- break;
- case 'IntersectionTypeAnnotation':
- ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case null:
- break;
- default:
- ignorePropsValidation = true;
- break;
- }
-
- components.set(node, {
- declaredPropTypes: declaredPropTypes,
- ignorePropsValidation: ignorePropsValidation
- });
- }
-
/**
* Used to recursively loop through each declared prop type
* @param {Object} component The component to process
@@ -891,12 +456,17 @@ module.exports = {
return;
}
- (props || []).forEach(prop => {
+ Object.keys(props || {}).forEach(key => {
+ const prop = props[key];
// Skip props that check instances
if (prop === true) {
return;
}
+ if (prop.type === 'shape' && configuration.skipShapeProps) {
+ return;
+ }
+
if (prop.node && !isPropUsed(component, prop)) {
context.report(
prop.node,
@@ -920,27 +490,6 @@ module.exports = {
reportUnusedPropType(component, component.declaredPropTypes);
}
- /**
- * Resolve the type annotation for a given node.
- * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
- * and `NullableTypeAnnotation` nodes which obscure the annotation we're
- * interested in.
- * This method also resolves type aliases where possible.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveTypeAnnotation(node) {
- let annotation = node.typeAnnotation || node;
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
/**
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
* FunctionDeclaration, or FunctionExpression
@@ -952,17 +501,6 @@ module.exports = {
}
}
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markAnnotatedFunctionArgumentsAsDeclared(node) {
- if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
- return;
- }
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
- }
-
function handleSetStateUpdater(node) {
if (!node.params || node.params.length < 2 || !inSetStateUpdater()) {
return;
@@ -978,7 +516,21 @@ module.exports = {
function handleFunctionLikeExpressions(node) {
handleSetStateUpdater(node);
markDestructuredFunctionArgumentsAsUsed(node);
- markAnnotatedFunctionArgumentsAsDeclared(node);
+ }
+
+ function handleCustomValidators(component) {
+ const propTypes = component.declaredPropTypes;
+ if (!propTypes) {
+ return;
+ }
+
+ Object.keys(propTypes).forEach(key => {
+ const node = propTypes[key].node;
+
+ if (astUtil.isFunctionLikeExpression(node)) {
+ markPropTypesAsUsed(node);
+ }
+ });
}
// --------------------------------------------------------------------------
@@ -986,20 +538,6 @@ module.exports = {
// --------------------------------------------------------------------------
return {
- ClassDeclaration: function(node) {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- },
-
- ClassProperty: function(node) {
- if (isAnnotatedClassPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- markPropTypesAsDeclared(node, node.value);
- }
- },
-
VariableDeclarator: function(node) {
const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
// let {props: {firstname}} = this
@@ -1023,50 +561,8 @@ module.exports = {
FunctionExpression: handleFunctionLikeExpressions,
MemberExpression: function(node) {
- let type;
if (isPropTypesUsage(node)) {
- type = 'usage';
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- type = 'declaration';
- }
-
- switch (type) {
- case 'usage':
- markPropTypesAsUsed(node);
- break;
- case 'declaration':
- const component = utils.getRelatedComponent(node);
- if (!component) {
- return;
- }
- markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
- break;
- default:
- break;
- }
- },
-
- JSXSpreadAttribute: function(node) {
- const component = components.get(utils.getParentComponent());
- components.set(component ? component.node : node, {
- ignorePropsValidation: true
- });
- },
-
- MethodDefinition: function(node) {
- if (!propsUtil.isPropTypesDeclaration(node)) {
- return;
- }
-
- let i = node.value.body.body.length - 1;
- for (; i >= 0; i--) {
- if (node.value.body.body[i].type === 'ReturnStatement') {
- break;
- }
- }
-
- if (i >= 0) {
- markPropTypesAsDeclared(node, node.value.body.body[i].argument);
+ markPropTypesAsUsed(node);
}
},
@@ -1082,40 +578,14 @@ module.exports = {
}
},
- ObjectExpression: function(node) {
- // Search for the proptypes declaration
- node.properties.forEach(property => {
- if (!propsUtil.isPropTypesDeclaration(property)) {
- return;
- }
- markPropTypesAsDeclared(node, property.value);
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
- },
-
'Program:exit': function() {
- stack = null;
const list = components.list();
// Report undeclared proptypes for all classes
for (const component in list) {
if (!has(list, component) || !mustBeValidated(list[component])) {
continue;
}
+ handleCustomValidators(list[component]);
reportUnusedPropTypes(list[component]);
}
}
diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js
index 1895727984..239c58bcec 100644
--- a/lib/rules/prop-types.js
+++ b/lib/rules/prop-types.js
@@ -9,10 +9,6 @@
const has = require('has');
const Components = require('../util/Components');
-const variable = require('../util/variable');
-const annotations = require('../util/annotations');
-const versionUtil = require('../util/version');
-const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
@@ -61,33 +57,11 @@ module.exports = {
create: Components.detect((context, components, utils) => {
const sourceCode = context.getSourceCode();
const configuration = context.options[0] || {};
- const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
const ignored = configuration.ignore || [];
- const customValidators = configuration.customValidators || [];
const skipUndeclared = configuration.skipUndeclared || false;
- // Used to track the type annotations in scope.
- // Necessary because babel's scopes do not track type annotations.
- let stack = null;
- const classExpressions = [];
const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
- /**
- * Helper for accessing the current scope in the stack.
- * @param {string} key The name of the identifier to access. If omitted, returns the full scope.
- * @param {ASTNode} value If provided sets the new value for the identifier.
- * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier.
- */
- function typeScope(key, value) {
- if (arguments.length === 0) {
- return stack[stack.length - 1];
- } else if (arguments.length === 1) {
- return stack[stack.length - 1][key];
- }
- stack[stack.length - 1][key] = value;
- return value;
- }
-
/**
* Check if we are in a class constructor
* @return {boolean} true if we are in a class constructor, false if not
@@ -168,41 +142,6 @@ module.exports = {
return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
}
- /**
- * Checks if we are declaring a `props` class property with a flow type annotation.
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if the node is a type annotated props declaration, false if not.
- */
- function isAnnotatedClassPropsDeclaration(node) {
- if (node && node.type === 'ClassProperty') {
- const tokens = context.getFirstTokens(node, 2);
- if (
- node.typeAnnotation && (
- tokens[0].value === 'props' ||
- (tokens[1] && tokens[1].value === 'props')
- )
- ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if we are declaring a props as a generic type in a flow-annotated class.
- *
- * @param {ASTNode} node the AST node being checked.
- * @returns {Boolean} True if the node is a class with generic prop types, false if not.
- */
- function isSuperTypeParameterPropsDeclaration(node) {
- if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
- if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
- return true;
- }
- }
- return false;
- }
-
/**
* Checks if the prop is ignored
* @param {String} name Name of the prop to check.
@@ -212,15 +151,6 @@ module.exports = {
return ignored.indexOf(name) !== -1;
}
- /**
- * Checks if prop should be validated by plugin-react-proptypes
- * @param {String} validator Name of validator to check.
- * @returns {Boolean} True if validator should be checked by custom validator.
- */
- function hasCustomValidator(validator) {
- return customValidators.indexOf(validator) !== -1;
- }
-
/**
* Checks if the component must be validated
* @param {Object} component The component to process
@@ -245,7 +175,6 @@ 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
@@ -258,7 +187,7 @@ module.exports = {
// If it's a computed property, we can't make any further analysis, but is valid
return key === '__COMPUTED_PROP__';
}
- if (typeof propType === 'object' && Object.keys(propType).length === 0) {
+ if (typeof propType === 'object' && !propType.type) {
return true;
}
// Consider every children as declared
@@ -353,210 +282,6 @@ module.exports = {
return key.type === 'Identifier' ? key.name : key.value;
}
- /**
- * Iterates through a properties node, like a customized forEach.
- * @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
- */
- function iterateProperties(properties, fn) {
- if (properties && properties.length && typeof fn === 'function') {
- for (let i = 0, j = properties.length; i < j; i++) {
- const node = properties[i];
- const key = getKeyValue(node);
-
- const value = node.value;
- fn(key, value);
- }
- }
- }
-
- /**
- * 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} The representation of the declaration, empty object means
- * the property is declared without the need for further analysis.
- */
- function buildReactDeclarationTypes(value) {
- if (
- value &&
- value.callee &&
- value.callee.object &&
- hasCustomValidator(value.callee.object.name)
- ) {
- return {};
- }
-
- if (
- value &&
- value.type === 'MemberExpression' &&
- value.property &&
- value.property.name &&
- value.property.name === 'isRequired'
- ) {
- value = value.object;
- }
-
- // Verify PropTypes that are functions
- if (
- value &&
- value.type === 'CallExpression' &&
- value.callee &&
- value.callee.property &&
- value.callee.property.name &&
- value.arguments &&
- value.arguments.length > 0
- ) {
- const callName = value.callee.property.name;
- const argument = value.arguments[0];
- switch (callName) {
- case 'shape':
- if (argument.type !== 'ObjectExpression') {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const shapeTypeDefinition = {
- type: 'shape',
- children: {}
- };
- iterateProperties(argument.properties, (childKey, childValue) => {
- shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue);
- });
- return shapeTypeDefinition;
- case 'arrayOf':
- case 'objectOf':
- return {
- type: 'object',
- children: {
- __ANY_KEY__: buildReactDeclarationTypes(argument)
- }
- };
- case 'oneOfType':
- if (
- !argument.elements ||
- !argument.elements.length
- ) {
- // Invalid proptype or cannot analyse statically
- return {};
- }
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = argument.elements.length; i < j; i++) {
- const type = buildReactDeclarationTypes(argument.elements[i]);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'instanceOf':
- return {
- type: 'instance',
- // Accept all children because we can't know what type they are
- children: true
- };
- case 'oneOf':
- default:
- return {};
- }
- }
- // Unknown property or accepts everything (any, object, ...)
- 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} The representation of the declaration, empty object means
- * the property is declared without the need for further analysis.
- */
- function buildTypeAnnotationDeclarationTypes(annotation, seen) {
- if (typeof seen === 'undefined') {
- // Keeps track of annotations we've already seen to
- // prevent problems with recursive types.
- seen = new Set();
- }
- if (seen.has(annotation)) {
- // This must be a recursive type annotation, so just accept anything.
- return {};
- }
- seen.add(annotation);
-
- switch (annotation.type) {
- case 'GenericTypeAnnotation':
- if (typeScope(annotation.id.name)) {
- return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name), seen);
- }
- return {};
- case 'ObjectTypeAnnotation':
- let containsObjectTypeSpread = false;
- const shapeTypeDefinition = {
- type: 'shape',
- children: {}
- };
- iterateProperties(annotation.properties, (childKey, childValue) => {
- if (!childKey && !childValue) {
- containsObjectTypeSpread = true;
- } else {
- shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue, seen);
- }
- });
-
- // nested object type spread means we need to ignore/accept everything in this object
- if (containsObjectTypeSpread) {
- return {};
- }
- return shapeTypeDefinition;
- case 'UnionTypeAnnotation':
- const unionTypeDefinition = {
- type: 'union',
- children: []
- };
- for (let i = 0, j = annotation.types.length; i < j; i++) {
- const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], seen);
- // keep only complex type
- if (Object.keys(type).length > 0) {
- if (type.children === true) {
- // every child is accepted for one type, abort type analysis
- unionTypeDefinition.children = true;
- return unionTypeDefinition;
- }
- }
-
- unionTypeDefinition.children.push(type);
- }
- if (unionTypeDefinition.children.length === 0) {
- // no complex type found, simply accept everything
- return {};
- }
- return unionTypeDefinition;
- case 'ArrayTypeAnnotation':
- return {
- type: 'object',
- children: {
- __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType, seen)
- }
- };
- default:
- // Unknown or accepts everything.
- return {};
- }
- }
-
/**
* Retrieve the name of a property node
* @param {ASTNode} node The AST node with the property.
@@ -730,152 +455,6 @@ 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);
- }
-
- if (annotation.type === 'UnionTypeAnnotation') {
- return true;
- }
-
- 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.
- * @param {propTypes} node The AST node containing the proptypes
- */
- function markPropTypesAsDeclared(node, propTypes) {
- let componentNode = node;
- while (componentNode && !components.get(componentNode)) {
- componentNode = componentNode.parent;
- }
- const component = components.get(componentNode);
- const declaredPropTypes = component && component.declaredPropTypes || {};
- let ignorePropsValidation = false;
-
- switch (propTypes && propTypes.type) {
- case 'ObjectTypeAnnotation':
- ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case 'ObjectExpression':
- iterateProperties(propTypes.properties, (key, value) => {
- if (!value) {
- ignorePropsValidation = true;
- return;
- }
- declaredPropTypes[key] = buildReactDeclarationTypes(value);
- });
- break;
- case 'MemberExpression':
- let curDeclaredPropTypes = declaredPropTypes;
- // Walk the list of properties, until we reach the assignment
- // ie: ClassX.propTypes.a.b.c = ...
- while (
- propTypes &&
- propTypes.parent &&
- propTypes.parent.type !== 'AssignmentExpression' &&
- propTypes.property &&
- curDeclaredPropTypes
- ) {
- const propName = propTypes.property.name;
- if (propName in curDeclaredPropTypes) {
- curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
- propTypes = propTypes.parent;
- } else {
- // This will crash at runtime because we haven't seen this key before
- // stop this and do not declare it
- propTypes = null;
- }
- }
- if (propTypes && propTypes.parent && propTypes.property) {
- curDeclaredPropTypes[propTypes.property.name] =
- buildReactDeclarationTypes(propTypes.parent.right);
- } else {
- ignorePropsValidation = true;
- }
- break;
- case 'Identifier':
- const variablesInScope = variable.variablesInScope(context);
- for (let i = 0, j = variablesInScope.length; i < j; i++) {
- if (variablesInScope[i].name !== propTypes.name) {
- continue;
- }
- const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
- markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
- return;
- }
- ignorePropsValidation = true;
- break;
- case 'CallExpression':
- if (
- propWrapperFunctions.has(sourceCode.getText(propTypes.callee)) &&
- propTypes.arguments && propTypes.arguments[0]
- ) {
- markPropTypesAsDeclared(node, propTypes.arguments[0]);
- return;
- }
- break;
- case 'IntersectionTypeAnnotation':
- ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
- break;
- case null:
- break;
- default:
- ignorePropsValidation = true;
- break;
- }
-
- components.set(node, {
- declaredPropTypes: declaredPropTypes,
- ignorePropsValidation: ignorePropsValidation
- });
- }
-
/**
* Reports undeclared proptypes for a given component
* @param {Object} component The component to process
@@ -899,55 +478,6 @@ module.exports = {
}
}
- /**
- * Resolve the type annotation for a given node.
- * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
- * and `NullableTypeAnnotation` nodes which obscure the annotation we're
- * interested in.
- * This method also resolves type aliases where possible.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveTypeAnnotation(node) {
- let annotation = node.typeAnnotation || node;
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
- /**
- * Resolve the type annotation for a given class declaration node with superTypeParameters.
- *
- * @param {ASTNode} node The annotation or a node containing the type annotation.
- * @returns {ASTNode} The resolved type annotation for the node.
- */
- function resolveSuperParameterPropsType(node) {
- let propsParameterPosition;
- try {
- // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
- // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
- propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
- } catch (e) {
- // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
- propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
- }
-
- let annotation = node.superTypeParameters.params[propsParameterPosition];
- while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
- annotation = annotation.typeAnnotation;
- }
-
- if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) {
- return typeScope(annotation.id.name);
- }
- return annotation;
- }
-
/**
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
* FunctionDeclaration, or FunctionExpression
@@ -959,52 +489,11 @@ module.exports = {
}
}
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function markAnnotatedFunctionArgumentsAsDeclared(node) {
- if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
- return;
- }
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
- }
-
- /**
- * @param {ASTNode} node We expect either an ArrowFunctionExpression,
- * FunctionDeclaration, or FunctionExpression
- */
- function handleStatelessComponent(node) {
- markDestructuredFunctionArgumentsAsUsed(node);
- markAnnotatedFunctionArgumentsAsDeclared(node);
- }
-
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
- ClassDeclaration: function(node) {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- },
-
- ClassExpression: function(node) {
- // TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
- // This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
- // processing class expressions until when the program exists.
- classExpressions.push(node);
- },
-
- ClassProperty: function(node) {
- if (isAnnotatedClassPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- markPropTypesAsDeclared(node, node.value);
- }
- },
-
VariableDeclarator: function(node) {
const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
// let {props: {firstname}} = this
@@ -1022,38 +511,20 @@ module.exports = {
markPropTypesAsUsed(node);
},
- FunctionDeclaration: handleStatelessComponent,
+ FunctionDeclaration: markDestructuredFunctionArgumentsAsUsed,
- ArrowFunctionExpression: handleStatelessComponent,
+ ArrowFunctionExpression: markDestructuredFunctionArgumentsAsUsed,
FunctionExpression: function(node) {
if (node.parent.type === 'MethodDefinition') {
return;
}
- handleStatelessComponent(node);
+ markDestructuredFunctionArgumentsAsUsed(node);
},
MemberExpression: function(node) {
- let type;
if (isPropTypesUsage(node)) {
- type = 'usage';
- } else if (propsUtil.isPropTypesDeclaration(node)) {
- type = 'declaration';
- }
-
- switch (type) {
- case 'usage':
- markPropTypesAsUsed(node);
- break;
- case 'declaration':
- const component = utils.getRelatedComponent(node);
- if (!component) {
- return;
- }
- markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
- break;
- default:
- break;
+ markPropTypesAsUsed(node);
}
},
@@ -1066,65 +537,9 @@ module.exports = {
if (node.key.name === 'shouldComponentUpdate' && destructuring) {
markPropTypesAsUsed(node);
}
-
- if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
- return;
- }
-
- let i = node.value.body.body.length - 1;
- for (; i >= 0; i--) {
- if (node.value.body.body[i].type === 'ReturnStatement') {
- break;
- }
- }
-
- if (i >= 0) {
- markPropTypesAsDeclared(node, node.value.body.body[i].argument);
- }
- },
-
- ObjectExpression: function(node) {
- // Search for the proptypes declaration
- node.properties.forEach(property => {
- if (!propsUtil.isPropTypesDeclaration(property)) {
- return;
- }
- markPropTypesAsDeclared(node, property.value);
- });
- },
-
- TypeAlias: function(node) {
- typeScope(node.id.name, node.right);
- },
-
- TypeParameterDeclaration: function(node) {
- const identifier = node.params[0];
-
- if (identifier.typeAnnotation) {
- typeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
- }
- },
-
- Program: function() {
- stack = [{}];
- },
-
- BlockStatement: function () {
- stack.push(Object.create(typeScope()));
- },
-
- 'BlockStatement:exit': function () {
- stack.pop();
},
'Program:exit': function() {
- classExpressions.forEach(node => {
- if (isSuperTypeParameterPropsDeclaration(node)) {
- markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
- }
- });
-
- stack = null;
const list = components.list();
// Report undeclared proptypes for all classes
for (const component in list) {
diff --git a/lib/util/Components.js b/lib/util/Components.js
index a9a4161bb6..1859600f35 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -10,12 +10,12 @@ const doctrine = require('doctrine');
const variableUtil = require('./variable');
const pragmaUtil = require('./pragma');
const astUtil = require('./ast');
+const propTypes = require('./propTypes');
function getId(node) {
return node && node.range.join(':');
}
-
function usedPropTypesAreEquivalent(propA, propB) {
if (propA.name === propB.name) {
if (!propA.allNames && !propB.allNames) {
@@ -695,12 +695,20 @@ function componentRule(rule, context) {
// Update the provided rule instructions to add the component detection
const ruleInstructions = rule(context, components, utils);
const updatedRuleInstructions = util._extend({}, ruleInstructions);
- Object.keys(detectionInstructions).forEach(instruction => {
+ const propTypesInstructions = propTypes(context, components, utils);
+ const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(propTypesInstructions)));
+ allKeys.forEach(instruction => {
updatedRuleInstructions[instruction] = function(node) {
- detectionInstructions[instruction](node);
+ if (instruction in detectionInstructions) {
+ detectionInstructions[instruction](node);
+ }
+ if (instruction in propTypesInstructions) {
+ propTypesInstructions[instruction](node);
+ }
return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0;
};
});
+
// Return the updated rule instructions
return updatedRuleInstructions;
}
diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js
new file mode 100644
index 0000000000..56b803bf7e
--- /dev/null
+++ b/lib/util/propTypes.js
@@ -0,0 +1,704 @@
+/**
+ * @fileoverview Common propTypes detection functionality.
+ */
+'use strict';
+
+const annotations = require('./annotations');
+const propsUtil = require('./props');
+const variableUtil = require('./variable');
+const versionUtil = require('./version');
+
+/**
+ * Checks if we are declaring a props as a generic type in a flow-annotated class.
+ *
+ * @param {ASTNode} node the AST node being checked.
+ * @returns {Boolean} True if the node is a class with generic prop types, false if not.
+ */
+function isSuperTypeParameterPropsDeclaration(node) {
+ if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
+ if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Removes quotes from around an identifier.
+ * @param {string} the identifier to strip
+ */
+function stripQuotes(string) {
+ return string.replace(/^\'|\'$/g, '');
+}
+
+/**
+ * Retrieve the name of a key node
+ * @param {ASTNode} node The AST node with the key.
+ * @return {string} the name of the key
+ */
+function getKeyValue(context, node) {
+ if (node.type === 'ObjectTypeProperty') {
+ const tokens = context.getFirstTokens(node, 2);
+ return (tokens[0].value === '+' || tokens[0].value === '-'
+ ? tokens[1].value
+ : stripQuotes(tokens[0].value)
+ );
+ }
+ const key = node.key || node.argument;
+ return key.type === 'Identifier' ? key.name : key.value;
+}
+
+/**
+ * Iterates through a properties node, like a customized forEach.
+ * @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
+ */
+function iterateProperties(context, properties, fn) {
+ 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);
+
+ const value = node.value;
+ fn(key, value);
+ }
+ }
+}
+
+module.exports = function propTypesInstructions(context, components, utils) {
+ // Used to track the type annotations in scope.
+ // Necessary because babel's scopes do not track type annotations.
+ let stack = null;
+
+ const classExpressions = [];
+ const defaults = {customValidators: []};
+ const configuration = Object.assign({}, defaults, context.options[0] || {});
+ const customValidators = configuration.customValidators;
+ const sourceCode = context.getSourceCode();
+ const propWrapperFunctions = new Set(context.settings.propWrapperFunctions);
+
+ /**
+ * Returns the full scope.
+ * @returns {Object} The whole scope.
+ */
+ function typeScope() {
+ return stack[stack.length - 1];
+ }
+
+ /**
+ * Gets a node from the scope.
+ * @param {string} key The name of the identifier to access.
+ * @returns {ASTNode} The ASTNode associated with the given identifier.
+ */
+ function getInTypeScope(key) {
+ return stack[stack.length - 1][key];
+ }
+
+ /**
+ * Sets the new value in the scope.
+ * @param {string} key The name of the identifier to access
+ * @param {ASTNode} value The new value for the identifier.
+ * @returns {ASTNode} The ASTNode associated with the given identifier.
+ */
+ function setInTypeScope(key, value) {
+ stack[stack.length - 1][key] = value;
+ return value;
+ }
+
+ /**
+ * Checks if prop should be validated by plugin-react-proptypes
+ * @param {String} validator Name of validator to check.
+ * @returns {Boolean} True if validator should be checked by custom validator.
+ */
+ function hasCustomValidator(validator) {
+ return customValidators.indexOf(validator) !== -1;
+ }
+
+ /* eslint-disable no-use-before-define */
+ const typeDeclarationBuilders = {
+ GenericTypeAnnotation: function(annotation, parentName, seen) {
+ if (getInTypeScope(annotation.id.name)) {
+ return buildTypeAnnotationDeclarationTypes(getInTypeScope(annotation.id.name), parentName, seen);
+ }
+ return {};
+ },
+
+ ObjectTypeAnnotation: function(annotation, parentName, seen) {
+ let containsObjectTypeSpread = false;
+ const shapeTypeDefinition = {
+ type: 'shape',
+ children: {}
+ };
+ iterateProperties(context, annotation.properties, (childKey, childValue) => {
+ const fullName = [parentName, childKey].join('.');
+ if (!childKey && !childValue) {
+ containsObjectTypeSpread = true;
+ } else {
+ const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = childValue;
+ shapeTypeDefinition.children[childKey] = types;
+ }
+ });
+
+ // nested object type spread means we need to ignore/accept everything in this object
+ if (containsObjectTypeSpread) {
+ return {};
+ }
+ return shapeTypeDefinition;
+ },
+
+ UnionTypeAnnotation: function(annotation, parentName, seen) {
+ const unionTypeDefinition = {
+ type: 'union',
+ children: []
+ };
+ for (let i = 0, j = annotation.types.length; i < j; i++) {
+ const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName, seen);
+ // keep only complex type
+ if (type.type) {
+ if (type.children === true) {
+ // every child is accepted for one type, abort type analysis
+ unionTypeDefinition.children = true;
+ return unionTypeDefinition;
+ }
+ }
+
+ unionTypeDefinition.children.push(type);
+ }
+ if (unionTypeDefinition.children.length === 0) {
+ // no complex type found, simply accept everything
+ return {};
+ }
+ return unionTypeDefinition;
+ },
+
+ ArrayTypeAnnotation: function(annotation, parentName, seen) {
+ const fullName = [parentName, '*'].join('.');
+ const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName, seen);
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = annotation;
+ return {
+ type: 'object',
+ children: {
+ __ANY_KEY__: child
+ }
+ };
+ }
+ };
+ /* eslint-enable no-use-before-define */
+
+ /**
+ * Resolve the type annotation for a given node.
+ * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
+ * and `NullableTypeAnnotation` nodes which obscure the annotation we're
+ * interested in.
+ * This method also resolves type aliases where possible.
+ *
+ * @param {ASTNode} node The annotation or a node containing the type annotation.
+ * @returns {ASTNode} The resolved type annotation for the node.
+ */
+ function resolveTypeAnnotation(node) {
+ let annotation = node.typeAnnotation || node;
+ while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
+ annotation = annotation.typeAnnotation;
+ }
+ if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
+ return getInTypeScope(annotation.id.name);
+ }
+ return annotation;
+ }
+
+ /**
+ * 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} The representation of the declaration, empty object means
+ * the property is declared without the need for further analysis.
+ */
+ function buildTypeAnnotationDeclarationTypes(annotation, parentName, seen) {
+ if (typeof seen === 'undefined') {
+ // Keeps track of annotations we've already seen to
+ // prevent problems with recursive types.
+ seen = new Set();
+ }
+ if (seen.has(annotation)) {
+ // This must be a recursive type annotation, so just accept anything.
+ return {};
+ }
+ seen.add(annotation);
+
+ if (annotation.type in typeDeclarationBuilders) {
+ return typeDeclarationBuilders[annotation.type](annotation, parentName, seen);
+ }
+ return {};
+ }
+
+ /**
+ * 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(context, propTypes.properties, (key, value) => {
+ if (!value) {
+ ignorePropsValidation = true;
+ return;
+ }
+
+ const types = buildTypeAnnotationDeclarationTypes(value, key);
+ types.fullName = key;
+ types.name = key;
+ types.node = value;
+ declaredPropTypes[key] = 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);
+ }
+
+ if (annotation.type === 'UnionTypeAnnotation') {
+ return true;
+ }
+
+ // Type can't be resolved
+ if (!annotation.id) {
+ return true;
+ }
+
+ const typeNode = getInTypeScope(annotation.id.name);
+
+ if (!typeNode) {
+ return true;
+ } else if (typeNode.type === 'IntersectionTypeAnnotation') {
+ return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
+ }
+
+ return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
+ });
+ }
+
+ /**
+ * 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} The representation of the declaration, empty object means
+ * the property is declared without the need for further analysis.
+ */
+ function buildReactDeclarationTypes(value, parentName) {
+ if (
+ value &&
+ value.callee &&
+ value.callee.object &&
+ hasCustomValidator(value.callee.object.name)
+ ) {
+ return {};
+ }
+
+ if (
+ value &&
+ value.type === 'MemberExpression' &&
+ value.property &&
+ value.property.name &&
+ value.property.name === 'isRequired'
+ ) {
+ value = value.object;
+ }
+
+ // Verify PropTypes that are functions
+ if (
+ value &&
+ value.type === 'CallExpression' &&
+ value.callee &&
+ value.callee.property &&
+ value.callee.property.name &&
+ value.arguments &&
+ value.arguments.length > 0
+ ) {
+ const callName = value.callee.property.name;
+ const argument = value.arguments[0];
+ switch (callName) {
+ case 'shape':
+ if (argument.type !== 'ObjectExpression') {
+ // Invalid proptype or cannot analyse statically
+ return {};
+ }
+ const shapeTypeDefinition = {
+ type: 'shape',
+ children: {}
+ };
+ iterateProperties(context, argument.properties, (childKey, childValue) => {
+ const fullName = [parentName, childKey].join('.');
+ const types = buildReactDeclarationTypes(childValue, fullName);
+ types.fullName = fullName;
+ types.name = childKey;
+ types.node = childValue;
+ shapeTypeDefinition.children[childKey] = types;
+ });
+ return shapeTypeDefinition;
+ case 'arrayOf':
+ case 'objectOf':
+ const fullName = [parentName, '*'].join('.');
+ const child = buildReactDeclarationTypes(argument, fullName);
+ child.fullName = fullName;
+ child.name = '__ANY_KEY__';
+ child.node = argument;
+ return {
+ type: 'object',
+ children: {
+ __ANY_KEY__: child
+ }
+ };
+ case 'oneOfType':
+ if (
+ !argument.elements ||
+ !argument.elements.length
+ ) {
+ // Invalid proptype or cannot analyse statically
+ return {};
+ }
+ const unionTypeDefinition = {
+ type: 'union',
+ children: []
+ };
+ for (let i = 0, j = argument.elements.length; i < j; i++) {
+ const type = buildReactDeclarationTypes(argument.elements[i], parentName);
+ // keep only complex type
+ if (type.type) {
+ if (type.children === true) {
+ // every child is accepted for one type, abort type analysis
+ unionTypeDefinition.children = true;
+ return unionTypeDefinition;
+ }
+ }
+
+ unionTypeDefinition.children.push(type);
+ }
+ if (unionTypeDefinition.length === 0) {
+ // no complex type found, simply accept everything
+ return {};
+ }
+ return unionTypeDefinition;
+ case 'instanceOf':
+ return {
+ type: 'instance',
+ // Accept all children because we can't know what type they are
+ children: true
+ };
+ case 'oneOf':
+ default:
+ return {};
+ }
+ }
+ // Unknown property or accepts everything (any, object, ...)
+ return {};
+ }
+
+
+ /**
+ * Mark a prop type as declared
+ * @param {ASTNode} node The AST node being checked.
+ * @param {propTypes} node The AST node containing the proptypes
+ */
+ function markPropTypesAsDeclared(node, propTypes) {
+ let componentNode = node;
+ while (componentNode && !components.get(componentNode)) {
+ componentNode = componentNode.parent;
+ }
+ const component = components.get(componentNode);
+ const declaredPropTypes = component && component.declaredPropTypes || {};
+ let ignorePropsValidation = component && component.ignorePropsValidation || false;
+
+ switch (propTypes && propTypes.type) {
+ case 'ObjectTypeAnnotation':
+ ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
+ break;
+ case 'ObjectExpression':
+ iterateProperties(context, propTypes.properties, (key, value) => {
+ if (!value) {
+ ignorePropsValidation = true;
+ return;
+ }
+ const types = buildReactDeclarationTypes(value, key);
+ types.fullName = key;
+ types.name = key;
+ types.node = value;
+ declaredPropTypes[key] = types;
+ });
+ break;
+ case 'MemberExpression':
+ let curDeclaredPropTypes = declaredPropTypes;
+ // Walk the list of properties, until we reach the assignment
+ // ie: ClassX.propTypes.a.b.c = ...
+ while (
+ propTypes &&
+ propTypes.parent &&
+ propTypes.parent.type !== 'AssignmentExpression' &&
+ propTypes.property &&
+ curDeclaredPropTypes
+ ) {
+ const propName = propTypes.property.name;
+ if (propName in curDeclaredPropTypes) {
+ curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
+ propTypes = propTypes.parent;
+ } else {
+ // This will crash at runtime because we haven't seen this key before
+ // stop this and do not declare it
+ propTypes = null;
+ }
+ }
+ if (propTypes && propTypes.parent && propTypes.property) {
+ const types = buildReactDeclarationTypes(
+ propTypes.parent.right,
+ propTypes.parent.left.object.property.name
+ );
+
+ types.name = propTypes.property.name;
+ types.fullName = propTypes.property.name;
+ types.node = propTypes.property;
+ curDeclaredPropTypes[propTypes.property.name] = types;
+ } else {
+ let isUsedInPropTypes = false;
+ let n = propTypes;
+ while (n) {
+ if (n.type === 'AssignmentExpression' && propsUtil.isPropTypesDeclaration(n.left) ||
+ (n.type === 'ClassProperty' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n)) {
+ // Found a propType used inside of another propType. This is not considered usage, we'll still validate
+ // this component.
+ isUsedInPropTypes = true;
+ break;
+ }
+ n = n.parent;
+ }
+ if (!isUsedInPropTypes) {
+ ignorePropsValidation = true;
+ }
+ }
+ break;
+ case 'Identifier':
+ const variablesInScope = variableUtil.variablesInScope(context);
+ for (let i = 0, j = variablesInScope.length; i < j; i++) {
+ if (variablesInScope[i].name !== propTypes.name) {
+ continue;
+ }
+ const defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1];
+ markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
+ return;
+ }
+ ignorePropsValidation = true;
+ break;
+ case 'CallExpression':
+ if (
+ propWrapperFunctions.has(sourceCode.getText(propTypes.callee)) &&
+ propTypes.arguments && propTypes.arguments[0]
+ ) {
+ markPropTypesAsDeclared(node, propTypes.arguments[0]);
+ return;
+ }
+ break;
+ case 'IntersectionTypeAnnotation':
+ ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
+ break;
+ case null:
+ break;
+ default:
+ ignorePropsValidation = true;
+ break;
+ }
+
+ components.set(node, {
+ declaredPropTypes: declaredPropTypes,
+ ignorePropsValidation: ignorePropsValidation
+ });
+ }
+
+ /**
+ * @param {ASTNode} node We expect either an ArrowFunctionExpression,
+ * FunctionDeclaration, or FunctionExpression
+ */
+ function markAnnotatedFunctionArgumentsAsDeclared(node) {
+ if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
+ return;
+ }
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0]));
+ }
+
+ /**
+ * Resolve the type annotation for a given class declaration node with superTypeParameters.
+ *
+ * @param {ASTNode} node The annotation or a node containing the type annotation.
+ * @returns {ASTNode} The resolved type annotation for the node.
+ */
+ function resolveSuperParameterPropsType(node) {
+ let propsParameterPosition;
+ try {
+ // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
+ // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
+ propsParameterPosition = versionUtil.testFlowVersion(context, '0.53.0') ? 0 : 1;
+ } catch (e) {
+ // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
+ propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
+ }
+
+ let annotation = node.superTypeParameters.params[propsParameterPosition];
+ while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
+ annotation = annotation.typeAnnotation;
+ }
+
+ if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
+ return getInTypeScope(annotation.id.name);
+ }
+ return annotation;
+ }
+
+ /**
+ * Checks if we are declaring a `props` class property with a flow type annotation.
+ * @param {ASTNode} node The AST node being checked.
+ * @returns {Boolean} True if the node is a type annotated props declaration, false if not.
+ */
+ function isAnnotatedClassPropsDeclaration(node) {
+ if (node && node.type === 'ClassProperty') {
+ const tokens = context.getFirstTokens(node, 2);
+ if (
+ node.typeAnnotation && (
+ tokens[0].value === 'props' ||
+ (tokens[1] && tokens[1].value === 'props')
+ )
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return {
+ ClassExpression: function(node) {
+ // TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
+ // This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
+ // processing class expressions until when the program exists.
+ classExpressions.push(node);
+ },
+
+ ClassDeclaration: function(node) {
+ if (isSuperTypeParameterPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
+ }
+ },
+
+ ClassProperty: function(node) {
+ if (isAnnotatedClassPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
+ } else if (propsUtil.isPropTypesDeclaration(node)) {
+ markPropTypesAsDeclared(node, node.value);
+ }
+ },
+
+ ObjectExpression: function(node) {
+ // Search for the proptypes declaration
+ node.properties.forEach(property => {
+ if (!propsUtil.isPropTypesDeclaration(property)) {
+ return;
+ }
+ markPropTypesAsDeclared(node, property.value);
+ });
+ },
+
+ FunctionExpression: function(node) {
+ if (node.parent.type !== 'MethodDefinition') {
+ markAnnotatedFunctionArgumentsAsDeclared(node);
+ }
+ },
+
+ FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
+
+ ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,
+
+ MemberExpression: function(node) {
+ if (propsUtil.isPropTypesDeclaration(node)) {
+ const component = utils.getRelatedComponent(node);
+ if (!component) {
+ return;
+ }
+ markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
+ }
+ },
+
+ MethodDefinition: function(node) {
+ if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
+ return;
+ }
+
+ let i = node.value.body.body.length - 1;
+ for (; i >= 0; i--) {
+ if (node.value.body.body[i].type === 'ReturnStatement') {
+ break;
+ }
+ }
+
+ if (i >= 0) {
+ markPropTypesAsDeclared(node, node.value.body.body[i].argument);
+ }
+ },
+
+ JSXSpreadAttribute: function(node) {
+ const component = components.get(utils.getParentComponent());
+ components.set(component ? component.node : node, {
+ ignorePropsValidation: true
+ });
+ },
+
+ TypeAlias: function(node) {
+ setInTypeScope(node.id.name, node.right);
+ },
+
+ TypeParameterDeclaration: function(node) {
+ const identifier = node.params[0];
+
+ if (identifier.typeAnnotation) {
+ setInTypeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
+ }
+ },
+
+ Program: function() {
+ stack = [{}];
+ },
+
+ BlockStatement: function () {
+ stack.push(Object.create(typeScope()));
+ },
+
+ 'BlockStatement:exit': function () {
+ stack.pop();
+ },
+
+ 'Program:exit': function() {
+ classExpressions.forEach(node => {
+ if (isSuperTypeParameterPropsDeclaration(node)) {
+ markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
+ }
+ });
+ }
+ };
+};
diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js
index 8f716a02ff..9222db7784 100644
--- a/tests/lib/rules/no-unused-prop-types.js
+++ b/tests/lib/rules/no-unused-prop-types.js
@@ -4327,6 +4327,60 @@ ruleTester.run('no-unused-prop-types', rule, {
}, {
message: '\'prop2.*\' PropType is defined but prop is never used'
}]
+ }, {
+ code: [
+ 'class Comp1 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp1.propTypes = {',
+ ' prop1: PropTypes.number',
+ '};',
+ 'class Comp2 extends Component {',
+ ' static propTypes = {',
+ ' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
+ ' }',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'prop1\' PropType is defined but prop is never used'
+ }, {
+ message: '\'prop2\' PropType is defined but prop is never used'
+ }, {
+ message: '\'prop2.*\' PropType is defined but prop is never used'
+ }]
+ }, {
+ code: [
+ 'class Comp1 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp1.propTypes = {',
+ ' prop1: PropTypes.number',
+ '};',
+ 'var Comp2 = createReactClass({',
+ ' propTypes: {',
+ ' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
+ ' },',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parser: 'babel-eslint',
+ errors: [{
+ message: '\'prop1\' PropType is defined but prop is never used'
+ }, {
+ message: '\'prop2\' PropType is defined but prop is never used'
+ }, {
+ message: '\'prop2.*\' PropType is defined but prop is never used'
+ }]
}, {
// Destructured assignment with Shape propTypes with skipShapeProps off issue #816
code: [
diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js
index 350bcedb13..6a7e31ae6b 100644
--- a/tests/lib/rules/prop-types.js
+++ b/tests/lib/rules/prop-types.js
@@ -586,6 +586,46 @@ ruleTester.run('prop-types', rule, {
'};'
].join('\n'),
parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Comp1 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp1.propTypes = {',
+ ' prop1: PropTypes.number',
+ '};',
+ 'class Comp2 extends Component {',
+ ' static propTypes = {',
+ ' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
+ ' }',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parser: 'babel-eslint'
+ }, {
+ code: [
+ 'class Comp1 extends Component {',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '}',
+ 'Comp1.propTypes = {',
+ ' prop1: PropTypes.number',
+ '};',
+ 'var Comp2 = createReactClass({',
+ ' propTypes: {',
+ ' prop2: PropTypes.arrayOf(Comp1.propTypes.prop1)',
+ ' },',
+ ' render() {',
+ ' return ;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parser: 'babel-eslint'
}, {
code: [
'const SomeComponent = createReactClass({',