blob: be8eb6501f134aa75472a0f569033c9a0d0256ff [file] [log] [blame]
/**
* @fileoverview Common propTypes detection functionality.
*/
'use strict';
const annotations = require('./annotations');
const propsUtil = require('./props');
const variableUtil = require('./variable');
const versionUtil = require('./version');
const propWrapperUtil = require('./propWrapper');
const getKeyValue = require('./ast').getKeyValue;
/**
* 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;
}
/**
* Iterates through a properties node, like a customized forEach.
* @param {Object} context Array of properties to iterate.
* @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, node);
}
}
}
/**
* Checks if a node is inside a class body.
*
* @param {ASTNode} node the AST node being checked.
* @returns {Boolean} True if the node has a ClassBody ancestor, false if not.
*/
function isInsideClassBody(node) {
let parent = node.parent;
while (parent) {
if (parent.type === 'ClassBody') {
return true;
}
parent = parent.parent;
}
return false;
}
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;
/**
* 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 */
/** @type {TypeDeclarationBuilders} */
const typeDeclarationBuilders = {
GenericTypeAnnotation(annotation, parentName, seen) {
if (getInTypeScope(annotation.id.name)) {
return buildTypeAnnotationDeclarationTypes(getInTypeScope(annotation.id.name), parentName, seen);
}
return {};
},
ObjectTypeAnnotation(annotation, parentName, seen) {
let containsObjectTypeSpread = false;
const shapeTypeDefinition = {
type: 'shape',
children: {}
};
iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => {
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 = propNode;
types.isRequired = !childValue.optional;
shapeTypeDefinition.children[childKey] = types;
}
});
// 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;
},
UnionTypeAnnotation(annotation, parentName, seen) {
/** @type {UnionTypeDefinition} */
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;
}
}
/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type);
}
if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).length === 0) {
// no complex type found, simply accept everything
return {};
}
return unionTypeDefinition;
},
ArrayTypeAnnotation(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.left && node.left.typeAnnotation) || 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.
* @param {String} parentName
* @param {Set<ASTNode>} [seen]
* @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, propNode) => {
if (!value) {
ignorePropsValidation = true;
return;
}
const types = buildTypeAnnotationDeclarationTypes(value, key);
types.fullName = key;
types.name = key;
types.node = propNode;
types.isRequired = !propNode.optional;
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;
}
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
* @param {string} parentName
* @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, propNode) => {
if (childValue) { // skip spread propTypes
const fullName = [parentName, childKey].join('.');
const types = buildReactDeclarationTypes(childValue, fullName);
types.fullName = fullName;
types.name = childKey;
types.node = propNode;
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 {};
}
/** @type {UnionTypeDefinition} */
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;
}
}
/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type);
}
if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).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 {ASTNode} propTypes 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, propNode) => {
if (!value) {
ignorePropsValidation = true;
return;
}
const types = buildReactDeclarationTypes(value, key);
types.fullName = key;
types.name = key;
types.node = propNode;
types.isRequired = propsUtil.isRequiredPropType(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) {
if (!(propTypes === propTypes.parent.left && propTypes.parent.left.object)) {
ignorePropsValidation = true;
break;
}
const parentProp = context.getSource(propTypes.parent.left.object).replace(/^.*\.propTypes\./, '');
const types = buildReactDeclarationTypes(
propTypes.parent.right,
parentProp
);
types.name = propTypes.property.name;
types.fullName = [parentProp, propTypes.property.name].join('.');
types.node = propTypes.parent;
types.isRequired = propsUtil.isRequiredPropType(propTypes.parent.right);
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);
const firstMatchingVariable = variablesInScope
.find(variableInScope => variableInScope.name === propTypes.name);
if (firstMatchingVariable) {
const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1];
markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
return;
}
ignorePropsValidation = true;
break;
}
case 'CallExpression': {
if (
propWrapperUtil.isPropWrapperFunction(
context,
context.getSourceCode().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,
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;
}
if (isInsideClassBody(node)) {
return;
}
const param = node.params[0];
if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') {
param.typeAnnotation.typeAnnotation.types.forEach((annotation) => {
if (annotation.type === 'GenericTypeAnnotation') {
markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation));
} else {
markPropTypesAsDeclared(node, annotation);
}
});
} else {
markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
}
}
/**
* 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 && 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(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(node) {
if (isSuperTypeParameterPropsDeclaration(node)) {
markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
}
},
ClassProperty(node) {
if (isAnnotatedClassPropsDeclaration(node)) {
markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
} else if (propsUtil.isPropTypesDeclaration(node)) {
markPropTypesAsDeclared(node, node.value);
}
},
ObjectExpression(node) {
// Search for the proptypes declaration
node.properties.forEach((property) => {
if (!propsUtil.isPropTypesDeclaration(property)) {
return;
}
markPropTypesAsDeclared(node, property.value);
});
},
FunctionExpression(node) {
if (node.parent.type !== 'MethodDefinition') {
markAnnotatedFunctionArgumentsAsDeclared(node);
}
},
FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,
MemberExpression(node) {
if (propsUtil.isPropTypesDeclaration(node)) {
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
}
},
MethodDefinition(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);
}
},
TypeAlias(node) {
setInTypeScope(node.id.name, node.right);
},
TypeParameterDeclaration(node) {
const identifier = node.params[0];
if (identifier.typeAnnotation) {
setInTypeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
}
},
Program() {
stack = [{}];
},
BlockStatement() {
stack.push(Object.create(typeScope()));
},
'BlockStatement:exit': function () {
stack.pop();
},
'Program:exit': function () {
classExpressions.forEach((node) => {
if (isSuperTypeParameterPropsDeclaration(node)) {
markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
}
});
}
};
};