1303 lines
45 KiB
Plaintext
1303 lines
45 KiB
Plaintext
/**
|
|
* @fileoverview Common propTypes detection functionality.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const flatMap = require('array.prototype.flatmap');
|
|
|
|
const annotations = require('./annotations');
|
|
const propsUtil = require('./props');
|
|
const variableUtil = require('./variable');
|
|
const testFlowVersion = require('./version').testFlowVersion;
|
|
const propWrapperUtil = require('./propWrapper');
|
|
const astUtil = require('./ast');
|
|
const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
|
|
const eslintUtil = require('./eslint');
|
|
|
|
const getFirstTokens = eslintUtil.getFirstTokens;
|
|
const getScope = eslintUtil.getScope;
|
|
const getSourceCode = eslintUtil.getSourceCode;
|
|
const getText = eslintUtil.getText;
|
|
|
|
/**
|
|
* Check if node is function type.
|
|
* @param {ASTNode} node
|
|
* @returns {boolean}
|
|
*/
|
|
function isFunctionType(node) {
|
|
if (!node) return false;
|
|
const nodeType = node.type;
|
|
return nodeType === 'FunctionDeclaration'
|
|
|| nodeType === 'FunctionExpression'
|
|
|| nodeType === 'ArrowFunctionExpression';
|
|
}
|
|
|
|
/**
|
|
* 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')) {
|
|
const parameters = propsUtil.getSuperTypeArguments(node);
|
|
if (parameters && parameters.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
|
|
* @param {Function} [handleSpreadFn] Function to call on each ObjectTypeSpreadProperty, receives the
|
|
argument
|
|
*/
|
|
function iterateProperties(context, properties, fn, handleSpreadFn) {
|
|
if (properties && properties.length && typeof fn === 'function') {
|
|
for (let i = 0, j = properties.length; i < j; i++) {
|
|
const node = properties[i];
|
|
const key = astUtil.getKeyValue(context, node);
|
|
|
|
if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') {
|
|
handleSpreadFn(node.argument);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function startWithCapitalizedLetter(node) {
|
|
return (
|
|
node.parent.type === 'VariableDeclarator'
|
|
&& !isFirstLetterCapitalized(node.parent.id.name)
|
|
);
|
|
}
|
|
|
|
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 allowedGenericTypes = new Set(['ComponentProps', 'ComponentPropsWithRef', 'ComponentPropsWithoutRef', 'forwardRef', 'ForwardRefRenderFunction', 'VFC', 'VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
|
|
const genericTypeParamIndexWherePropsArePresent = {
|
|
ComponentProps: 0,
|
|
ComponentPropsWithRef: 0,
|
|
ComponentPropsWithoutRef: 0,
|
|
ForwardRefRenderFunction: 1,
|
|
forwardRef: 1,
|
|
VoidFunctionComponent: 0,
|
|
VFC: 0,
|
|
PropsWithChildren: 0,
|
|
SFC: 0,
|
|
StatelessComponent: 0,
|
|
FunctionComponent: 0,
|
|
FC: 0,
|
|
};
|
|
const genericReactTypesImport = new Set();
|
|
// import { FC as X } from 'react' -> localToImportedMap = { x: FC }
|
|
const localToImportedMap = {};
|
|
|
|
/**
|
|
* 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 containsUnresolvedObjectTypeSpread = false;
|
|
let containsSpread = false;
|
|
const containsIndexers = !!annotation.indexers && annotation.indexers.length > 0;
|
|
const shapeTypeDefinition = {
|
|
type: 'shape',
|
|
children: {},
|
|
};
|
|
iterateProperties(
|
|
context,
|
|
annotation.properties,
|
|
(childKey, childValue, propNode) => {
|
|
const fullName = [parentName, childKey].join('.');
|
|
if (childKey || childValue) {
|
|
const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
|
|
types.fullName = fullName;
|
|
types.name = childKey;
|
|
types.node = propNode;
|
|
types.isRequired = !childValue.optional;
|
|
shapeTypeDefinition.children[childKey] = types;
|
|
}
|
|
},
|
|
(spreadNode) => {
|
|
const key = astUtil.getKeyValue(context, spreadNode);
|
|
const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen);
|
|
if (!types.children) {
|
|
containsUnresolvedObjectTypeSpread = true;
|
|
} else {
|
|
Object.assign(shapeTypeDefinition, types.children);
|
|
}
|
|
containsSpread = true;
|
|
}
|
|
);
|
|
|
|
// Mark if this shape has spread or an indexer. 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.containsUnresolvedSpread = containsUnresolvedObjectTypeSpread;
|
|
shapeTypeDefinition.containsIndexers = containsIndexers;
|
|
// Deprecated: containsSpread is not used anymore in the codebase, ensure to keep API backward compatibility
|
|
shapeTypeDefinition.containsSpread = containsSpread;
|
|
|
|
return shapeTypeDefinition;
|
|
},
|
|
|
|
UnionTypeAnnotation(annotation, parentName, seen) {
|
|
/** @type {UnionTypeDefinition} */
|
|
const unionTypeDefinition = {
|
|
type: 'union',
|
|
children: annotation.types.map((type) => buildTypeAnnotationDeclarationTypes(type, parentName, seen)),
|
|
};
|
|
if (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 ObjectTypeAnnotation 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 = ignorePropsValidation || propNode.type !== 'ObjectTypeSpreadProperty';
|
|
return;
|
|
}
|
|
|
|
const types = buildTypeAnnotationDeclarationTypes(value, key);
|
|
types.fullName = key;
|
|
types.name = key;
|
|
types.node = propNode;
|
|
types.isRequired = !propNode.optional;
|
|
declaredPropTypes[key] = types;
|
|
}, (spreadNode) => {
|
|
const key = astUtil.getKeyValue(context, spreadNode);
|
|
const spreadAnnotation = getInTypeScope(key);
|
|
if (!spreadAnnotation) {
|
|
ignorePropsValidation = true;
|
|
} else {
|
|
const spreadIgnoreValidation = declarePropTypesForObjectTypeAnnotation(spreadAnnotation, declaredPropTypes);
|
|
ignorePropsValidation = ignorePropsValidation || spreadIgnoreValidation;
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolve node of type Identifier when building declaration types.
|
|
* @param {ASTNode} node
|
|
* @param {ASTNode} rootNode
|
|
* @param {Function} callback called with the resolved value only if resolved.
|
|
*/
|
|
function resolveValueForIdentifierNode(node, rootNode, callback) {
|
|
if (
|
|
rootNode
|
|
&& node
|
|
&& node.type === 'Identifier'
|
|
) {
|
|
const scope = getScope(context, rootNode);
|
|
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.
|
|
* @param {ASTNode} value Node of the PropTypes for the desired property
|
|
* @param {string} parentName
|
|
* @param {ASTNode} rootNode
|
|
* @return {Object} The representation of the declaration, empty object means
|
|
* the property is declared without the need for further analysis.
|
|
*/
|
|
function buildReactDeclarationTypes(value, parentName, rootNode) {
|
|
if (
|
|
value
|
|
&& value.callee
|
|
&& value.callee.object
|
|
&& hasCustomValidator(value.callee.object.name)
|
|
) {
|
|
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, rootNode, (newValue) => {
|
|
identNodeResolved = true;
|
|
value = newValue;
|
|
});
|
|
|
|
if (
|
|
value
|
|
&& value.type === 'MemberExpression'
|
|
&& value.property
|
|
&& value.property.name === 'isRequired'
|
|
) {
|
|
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, rootNode, (newValue) => {
|
|
value = newValue;
|
|
});
|
|
}
|
|
|
|
// Verify PropTypes that are functions
|
|
if (
|
|
astUtil.isCallExpression(value)
|
|
&& 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':
|
|
case 'exact': {
|
|
if (argument.type !== 'ObjectExpression') {
|
|
// Invalid proptype or cannot analyse statically
|
|
return {};
|
|
}
|
|
const shapeTypeDefinition = {
|
|
type: callName,
|
|
children: {},
|
|
};
|
|
iterateProperties(context, argument.properties, (childKey, childValue, propNode) => {
|
|
if (childValue) { // skip spread propTypes
|
|
const fullName = [parentName, childKey].join('.');
|
|
const types = buildReactDeclarationTypes(childValue, fullName, rootNode);
|
|
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, rootNode);
|
|
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 === 0
|
|
) {
|
|
// Invalid proptype or cannot analyse statically
|
|
return {};
|
|
}
|
|
|
|
/** @type {UnionTypeDefinition} */
|
|
const unionTypeDefinition = {
|
|
type: 'union',
|
|
children: argument.elements.map((element) => buildReactDeclarationTypes(element, parentName, rootNode)),
|
|
};
|
|
if (unionTypeDefinition.children.length === 0) {
|
|
// no complex type found, simply accept everything
|
|
return {};
|
|
}
|
|
return unionTypeDefinition;
|
|
}
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
// Unknown property or accepts everything (any, object, ...)
|
|
return {};
|
|
}
|
|
|
|
function isValidReactGenericTypeAnnotation(annotation) {
|
|
if (annotation.typeName) {
|
|
if (annotation.typeName.name) { // if FC<Props>
|
|
const typeName = annotation.typeName.name;
|
|
if (!genericReactTypesImport.has(typeName)) {
|
|
return false;
|
|
}
|
|
} else if (annotation.typeName.right.name) { // if React.FC<Props>
|
|
const right = annotation.typeName.right.name;
|
|
const left = annotation.typeName.left.name;
|
|
|
|
if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the left most typeName of a node, e.g: FC<Props>, React.FC<Props>
|
|
* The representation is used to verify nested used properties.
|
|
* @param {ASTNode} node
|
|
* @return {string | undefined}
|
|
*/
|
|
function getLeftMostTypeName(node) {
|
|
if (node.name) return node.name;
|
|
if (node.left) return getLeftMostTypeName(node.left);
|
|
}
|
|
|
|
function getRightMostTypeName(node) {
|
|
if (node.name) return node.name;
|
|
if (node.right) return getRightMostTypeName(node.right);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the node is either a interface or type alias declaration
|
|
* @param {ASTNode} node
|
|
* @return {boolean}
|
|
*/
|
|
function filterInterfaceOrTypeAlias(node) {
|
|
return (
|
|
astUtil.isTSInterfaceDeclaration(node) || astUtil.isTSTypeAliasDeclaration(node)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the interface or type alias declaration node name matches the type-name str
|
|
* @param {ASTNode} node
|
|
* @param {string} typeName
|
|
* @return {boolean}
|
|
*/
|
|
function filterInterfaceOrAliasByName(node, typeName) {
|
|
return (
|
|
node.id
|
|
&& node.id.name === typeName
|
|
) || (
|
|
node.declaration
|
|
&& node.declaration.id
|
|
&& node.declaration.id.name === typeName
|
|
);
|
|
}
|
|
|
|
class DeclarePropTypesForTSTypeAnnotation {
|
|
constructor(propTypes, declaredPropTypes, rootNode) {
|
|
this.propTypes = propTypes;
|
|
this.declaredPropTypes = declaredPropTypes;
|
|
this.foundDeclaredPropertiesList = [];
|
|
this.referenceNameMap = new Set();
|
|
this.sourceCode = getSourceCode(context);
|
|
this.shouldIgnorePropTypes = false;
|
|
this.rootNode = rootNode;
|
|
this.visitTSNode(this.propTypes);
|
|
this.endAndStructDeclaredPropTypes();
|
|
}
|
|
|
|
/**
|
|
* The node will be distribute to different function.
|
|
* @param {ASTNode} node
|
|
*/
|
|
visitTSNode(node) {
|
|
if (!node) return;
|
|
if (astUtil.isTSTypeAnnotation(node)) {
|
|
const typeAnnotation = node.typeAnnotation;
|
|
this.visitTSNode(typeAnnotation);
|
|
} else if (astUtil.isTSTypeReference(node)) {
|
|
this.searchDeclarationByName(node);
|
|
} else if (astUtil.isTSInterfaceHeritage(node)) {
|
|
this.searchDeclarationByName(node);
|
|
} else if (astUtil.isTSTypeLiteral(node)) {
|
|
// Check node is an object literal
|
|
if (Array.isArray(node.members)) {
|
|
this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.members);
|
|
}
|
|
} else if (astUtil.isTSIntersectionType(node)) {
|
|
this.convertIntersectionTypeToPropTypes(node);
|
|
} else if (astUtil.isTSParenthesizedType(node)) {
|
|
const typeAnnotation = node.typeAnnotation;
|
|
this.visitTSNode(typeAnnotation);
|
|
} else if (astUtil.isTSTypeParameterInstantiation(node)) {
|
|
if (Array.isArray(node.params)) {
|
|
node.params.forEach((x) => this.visitTSNode(x));
|
|
}
|
|
} else {
|
|
this.shouldIgnorePropTypes = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search TSInterfaceDeclaration or TSTypeAliasDeclaration,
|
|
* by using TSTypeReference and TSInterfaceHeritage name.
|
|
* @param {ASTNode} node
|
|
*/
|
|
searchDeclarationByName(node) {
|
|
let typeName;
|
|
if (astUtil.isTSTypeReference(node)) {
|
|
typeName = node.typeName.name;
|
|
const leftMostName = getLeftMostTypeName(node.typeName);
|
|
const shouldTraverseTypeParams = genericReactTypesImport.has(leftMostName);
|
|
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
|
if (shouldTraverseTypeParams && nodeTypeArguments && nodeTypeArguments.length !== 0) {
|
|
// All react Generic types are derived from:
|
|
// type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
|
|
// So we should construct an optional children prop
|
|
this.shouldSpecifyOptionalChildrenProps = true;
|
|
|
|
const rightMostName = getRightMostTypeName(node.typeName);
|
|
if (
|
|
leftMostName === 'React'
|
|
&& (
|
|
rightMostName === 'HTMLAttributes'
|
|
|| rightMostName === 'HTMLElement'
|
|
|| rightMostName === 'HTMLProps'
|
|
)
|
|
) {
|
|
this.shouldSpecifyClassNameProp = true;
|
|
}
|
|
|
|
const importedName = localToImportedMap[rightMostName];
|
|
const idx = genericTypeParamIndexWherePropsArePresent[
|
|
leftMostName !== rightMostName ? rightMostName : importedName
|
|
];
|
|
const nextNode = nodeTypeArguments.params[idx];
|
|
this.visitTSNode(nextNode);
|
|
return;
|
|
}
|
|
} else if (astUtil.isTSInterfaceHeritage(node)) {
|
|
if (!node.expression && node.id) {
|
|
typeName = node.id.name;
|
|
} else {
|
|
typeName = node.expression.name;
|
|
}
|
|
}
|
|
if (!typeName) {
|
|
this.shouldIgnorePropTypes = true;
|
|
return;
|
|
}
|
|
if (typeName === 'ReturnType') {
|
|
this.convertReturnTypeToPropTypes(node, this.rootNode);
|
|
return;
|
|
}
|
|
// Prevent recursive inheritance will cause maximum callstack.
|
|
if (this.referenceNameMap.has(typeName)) {
|
|
this.shouldIgnorePropTypes = true;
|
|
return;
|
|
}
|
|
// Add typeName to Set and consider it as traversed.
|
|
this.referenceNameMap.add(typeName);
|
|
|
|
/**
|
|
* From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser
|
|
* Need to be deprecated after remove typescript-eslint-parser support.
|
|
*/
|
|
const candidateTypes = this.sourceCode.ast.body.filter((item) => astUtil.isTSTypeDeclaration(item));
|
|
|
|
const declarations = flatMap(
|
|
candidateTypes,
|
|
(type) => (
|
|
type.declarations
|
|
|| (
|
|
type.declaration
|
|
&& type.declaration.declarations
|
|
)
|
|
|| type.declaration
|
|
)
|
|
);
|
|
|
|
// we tried to find either an interface or a type with the TypeReference name
|
|
const typeDeclaration = declarations.filter((dec) => dec.id.name === typeName);
|
|
|
|
const interfaceDeclarations = this.sourceCode.ast.body
|
|
.filter(filterInterfaceOrTypeAlias)
|
|
.filter((item) => filterInterfaceOrAliasByName(item, typeName))
|
|
.map((item) => (item.declaration || item));
|
|
|
|
if (typeDeclaration.length !== 0) {
|
|
typeDeclaration.map((t) => t.init || t.typeAnnotation).forEach(this.visitTSNode, this);
|
|
} else if (interfaceDeclarations.length !== 0) {
|
|
interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this);
|
|
} else {
|
|
this.shouldIgnorePropTypes = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Traverse TSInterfaceDeclaration and TSTypeAliasDeclaration
|
|
* which retrieve from function searchDeclarationByName;
|
|
* @param {ASTNode} node
|
|
*/
|
|
traverseDeclaredInterfaceOrTypeAlias(node) {
|
|
if (astUtil.isTSInterfaceDeclaration(node)) {
|
|
// Handle TSInterfaceDeclaration interface Props { name: string, id: number}, should put in properties list directly;
|
|
this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.body.body);
|
|
}
|
|
// Handle TSTypeAliasDeclaration type Props = {name:string}
|
|
if (astUtil.isTSTypeAliasDeclaration(node)) {
|
|
const typeAnnotation = node.typeAnnotation;
|
|
this.visitTSNode(typeAnnotation);
|
|
}
|
|
if (Array.isArray(node.extends)) {
|
|
node.extends.forEach((x) => this.visitTSNode(x));
|
|
// This line is trying to handle typescript-eslint-parser
|
|
// typescript-eslint-parser extension is name as heritage
|
|
} else if (Array.isArray(node.heritage)) {
|
|
node.heritage.forEach((x) => this.visitTSNode(x));
|
|
}
|
|
}
|
|
|
|
convertIntersectionTypeToPropTypes(node) {
|
|
if (!node) return;
|
|
if (Array.isArray(node.types)) {
|
|
node.types.forEach((x) => this.visitTSNode(x));
|
|
} else {
|
|
this.shouldIgnorePropTypes = true;
|
|
}
|
|
}
|
|
|
|
convertReturnTypeToPropTypes(node, rootNode) {
|
|
// ReturnType<T> should always have one parameter
|
|
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
|
if (nodeTypeArguments) {
|
|
if (nodeTypeArguments.params.length === 1) {
|
|
let returnType = nodeTypeArguments.params[0];
|
|
// This line is trying to handle typescript-eslint-parser
|
|
// typescript-eslint-parser TSTypeQuery is wrapped by TSTypeReference
|
|
if (astUtil.isTSTypeReference(returnType)) {
|
|
returnType = returnType.typeName;
|
|
}
|
|
// Handle ReturnType<typeof mapStateToProps>
|
|
if (astUtil.isTSTypeQuery(returnType)) {
|
|
const returnTypeFunction = flatMap(this.sourceCode.ast.body
|
|
.filter((item) => item.type === 'VariableDeclaration'
|
|
&& item.declarations.find((dec) => dec.id.name === returnType.exprName.name)
|
|
), (type) => type.declarations).map((dec) => dec.init);
|
|
|
|
if (Array.isArray(returnTypeFunction)) {
|
|
if (returnTypeFunction.length === 0) {
|
|
// Cannot find identifier in current scope. It might be an exported type.
|
|
this.shouldIgnorePropTypes = true;
|
|
return;
|
|
}
|
|
returnTypeFunction.forEach((func) => {
|
|
if (isFunctionType(func)) {
|
|
let res = func.body;
|
|
if (res.type === 'BlockStatement') {
|
|
res = astUtil.findReturnStatement(func);
|
|
if (res) {
|
|
res = res.argument;
|
|
}
|
|
}
|
|
switch (res.type) {
|
|
case 'ObjectExpression':
|
|
iterateProperties(context, res.properties, (key, value, propNode) => {
|
|
if (propNode && astUtil.isCallExpression(propNode.argument)) {
|
|
const propNodeTypeArguments = propsUtil.getTypeArguments(propNode.argument);
|
|
if (propNodeTypeArguments) {
|
|
this.visitTSNode(propNodeTypeArguments);
|
|
} else {
|
|
// Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types.
|
|
this.shouldIgnorePropTypes = true;
|
|
return;
|
|
}
|
|
}
|
|
if (!value) {
|
|
this.shouldIgnorePropTypes = true;
|
|
return;
|
|
}
|
|
const types = buildReactDeclarationTypes(value, key, rootNode);
|
|
types.fullName = key;
|
|
types.name = key;
|
|
types.node = propNode;
|
|
types.isRequired = propsUtil.isRequiredPropType(value);
|
|
this.declaredPropTypes[key] = types;
|
|
});
|
|
break;
|
|
case 'CallExpression':
|
|
if (propsUtil.getTypeArguments(res)) {
|
|
this.visitTSNode(propsUtil.getTypeArguments(res));
|
|
} else {
|
|
// Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types.
|
|
this.shouldIgnorePropTypes = true;
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
// Handle ReturnType<()=>returnType>
|
|
if (astUtil.isTSFunctionType(returnType)) {
|
|
if (astUtil.isTSTypeAnnotation(returnType.returnType)) {
|
|
this.visitTSNode(returnType.returnType);
|
|
return;
|
|
}
|
|
// This line is trying to handle typescript-eslint-parser
|
|
// typescript-eslint-parser TSFunction name returnType as typeAnnotation
|
|
if (astUtil.isTSTypeAnnotation(returnType.typeAnnotation)) {
|
|
this.visitTSNode(returnType.typeAnnotation);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.shouldIgnorePropTypes = true;
|
|
}
|
|
|
|
endAndStructDeclaredPropTypes() {
|
|
if (this.shouldSpecifyOptionalChildrenProps) {
|
|
this.declaredPropTypes.children = {
|
|
fullName: 'children',
|
|
name: 'children',
|
|
isRequired: false,
|
|
};
|
|
}
|
|
if (this.shouldSpecifyClassNameProp) {
|
|
this.declaredPropTypes.className = {
|
|
fullName: 'className',
|
|
name: 'className',
|
|
isRequired: false,
|
|
};
|
|
}
|
|
|
|
this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => {
|
|
if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) {
|
|
let accessor = 'name';
|
|
if (tsInterfaceBody.key.type === 'Literal') {
|
|
if (typeof tsInterfaceBody.key.value === 'number') {
|
|
accessor = 'raw';
|
|
} else {
|
|
accessor = 'value';
|
|
}
|
|
}
|
|
this.declaredPropTypes[tsInterfaceBody.key[accessor]] = {
|
|
fullName: tsInterfaceBody.key[accessor],
|
|
name: tsInterfaceBody.key[accessor],
|
|
node: tsInterfaceBody,
|
|
isRequired: !tsInterfaceBody.optional,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark a prop type as declared
|
|
* @param {ASTNode} node The AST node being checked.
|
|
* @param {ASTNode} propTypes The AST node containing the proptypes
|
|
* @param {ASTNode} rootNode
|
|
*/
|
|
function markPropTypesAsDeclared(node, propTypes, rootNode) {
|
|
let componentNode = node;
|
|
while (componentNode && !components.get(componentNode)) {
|
|
componentNode = componentNode.parent;
|
|
}
|
|
const component = components.get(componentNode);
|
|
let 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, rootNode);
|
|
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 = getText(context, propTypes.parent.left.object).replace(/^.*\.propTypes\./, '');
|
|
const types = buildReactDeclarationTypes(
|
|
propTypes.parent.right,
|
|
parentProp,
|
|
rootNode
|
|
);
|
|
|
|
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 === 'PropertyDefinition' || 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 firstMatchingVariable = variableUtil.getVariableFromContext(context, node, propTypes.name);
|
|
if (firstMatchingVariable) {
|
|
const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1];
|
|
markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init, rootNode);
|
|
return;
|
|
}
|
|
ignorePropsValidation = true;
|
|
break;
|
|
}
|
|
case 'CallExpression': {
|
|
if (
|
|
propWrapperUtil.isPropWrapperFunction(
|
|
context,
|
|
getText(context, propTypes.callee)
|
|
)
|
|
&& propTypes.arguments && propTypes.arguments[0]
|
|
) {
|
|
markPropTypesAsDeclared(node, propTypes.arguments[0], rootNode);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case 'IntersectionTypeAnnotation':
|
|
ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
|
|
break;
|
|
case 'GenericTypeAnnotation':
|
|
if (propTypes.id.name === '$ReadOnly') {
|
|
const propTypeArguments = propsUtil.getTypeArguments(propTypes);
|
|
ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(
|
|
propTypeArguments.params[0],
|
|
declaredPropTypes
|
|
);
|
|
} else {
|
|
ignorePropsValidation = true;
|
|
}
|
|
break;
|
|
case 'TSTypeReference':
|
|
case 'TSTypeAnnotation': {
|
|
const tsTypeAnnotation = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes, rootNode);
|
|
ignorePropsValidation = tsTypeAnnotation.shouldIgnorePropTypes;
|
|
declaredPropTypes = tsTypeAnnotation.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
|
|
* @param {ASTNode} rootNode
|
|
*/
|
|
function markAnnotatedFunctionArgumentsAsDeclared(node, rootNode) {
|
|
if (!node.params || !node.params.length) {
|
|
return;
|
|
}
|
|
|
|
let propTypesArguments = null;
|
|
if (node.parent) {
|
|
propTypesArguments = propsUtil.getTypeArguments(node.parent);
|
|
}
|
|
|
|
if (
|
|
node.parent
|
|
&& node.parent.callee
|
|
&& propTypesArguments
|
|
&& propTypesArguments.params
|
|
&& (
|
|
node.parent.callee.name === 'forwardRef' || (
|
|
node.parent.callee.object
|
|
&& node.parent.callee.property
|
|
&& node.parent.callee.object.name === 'React'
|
|
&& node.parent.callee.property.name === 'forwardRef'
|
|
)
|
|
)
|
|
) {
|
|
const declaredPropTypes = {};
|
|
const obj = new DeclarePropTypesForTSTypeAnnotation(propTypesArguments.params[1], declaredPropTypes, rootNode);
|
|
components.set(node, {
|
|
declaredPropTypes: obj.declaredPropTypes,
|
|
ignorePropsValidation: obj.shouldIgnorePropTypes,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const siblingIdentifier = node.parent && node.parent.id;
|
|
const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation;
|
|
const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context);
|
|
|
|
if (!isNodeAnnotated && !siblingHasTypeAnnotation) {
|
|
return;
|
|
}
|
|
|
|
// https://github.com/jsx-eslint/eslint-plugin-react/issues/2784
|
|
if (isInsideClassBody(node) && !astUtil.isFunction(node)) {
|
|
return;
|
|
}
|
|
|
|
// Should ignore function that not return JSXElement
|
|
if (!utils.isReturningJSXOrNull(node) || startWithCapitalizedLetter(node)) {
|
|
return;
|
|
}
|
|
|
|
if (isNodeAnnotated) {
|
|
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), rootNode);
|
|
} else {
|
|
markPropTypesAsDeclared(node, annotation, rootNode);
|
|
}
|
|
});
|
|
} else {
|
|
markPropTypesAsDeclared(node, resolveTypeAnnotation(param), rootNode);
|
|
}
|
|
} else {
|
|
// implements what's discussed here: https://github.com/jsx-eslint/eslint-plugin-react/issues/2777#issuecomment-683944481
|
|
const annotation = siblingIdentifier.typeAnnotation.typeAnnotation;
|
|
|
|
if (
|
|
annotation
|
|
&& annotation.type !== 'TSTypeReference'
|
|
&& propsUtil.getTypeArguments(annotation) == null
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!isValidReactGenericTypeAnnotation(annotation)) return;
|
|
|
|
markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier), rootNode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the type annotation for a given class declaration node.
|
|
*
|
|
* @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;
|
|
const parameters = propsUtil.getSuperTypeArguments(node);
|
|
|
|
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 = 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 = parameters.params.length <= 2 ? 0 : 1;
|
|
}
|
|
|
|
let annotation = parameters.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' || node.type === 'PropertyDefinition')) {
|
|
const tokens = getFirstTokens(context, 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), node);
|
|
}
|
|
},
|
|
|
|
'ClassProperty, PropertyDefinition'(node) {
|
|
if (isAnnotatedClassPropsDeclaration(node)) {
|
|
markPropTypesAsDeclared(node, resolveTypeAnnotation(node), node);
|
|
} else if (propsUtil.isPropTypesDeclaration(node)) {
|
|
markPropTypesAsDeclared(node, node.value, node);
|
|
}
|
|
},
|
|
|
|
ObjectExpression(node) {
|
|
// Search for the proptypes declaration
|
|
node.properties.forEach((property) => {
|
|
if (!propsUtil.isPropTypesDeclaration(property)) {
|
|
return;
|
|
}
|
|
markPropTypesAsDeclared(node, property.value, node);
|
|
});
|
|
},
|
|
|
|
FunctionExpression(node) {
|
|
if (node.parent.type !== 'MethodDefinition') {
|
|
markAnnotatedFunctionArgumentsAsDeclared(node, node);
|
|
}
|
|
},
|
|
|
|
ImportDeclaration(node) {
|
|
// parse `import ... from 'react`
|
|
if (node.source.value === 'react') {
|
|
node.specifiers.forEach((specifier) => {
|
|
if (
|
|
// handles import * as X from 'react'
|
|
specifier.type === 'ImportNamespaceSpecifier'
|
|
// handles import React from 'react'
|
|
|| specifier.type === 'ImportDefaultSpecifier'
|
|
) {
|
|
genericReactTypesImport.add(specifier.local.name);
|
|
}
|
|
|
|
// handles import { FC } from 'react' or import { FC as X } from 'react'
|
|
if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) {
|
|
genericReactTypesImport.add(specifier.local.name);
|
|
localToImportedMap[specifier.local.name] = specifier.imported.name;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
|
|
|
|
ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,
|
|
|
|
MemberExpression(node) {
|
|
if (propsUtil.isPropTypesDeclaration(node)) {
|
|
const component = utils.getRelatedComponent(node);
|
|
if (!component) {
|
|
return;
|
|
}
|
|
try {
|
|
markPropTypesAsDeclared(component.node, node.parent.right || node.parent, node);
|
|
} catch (e) {
|
|
if (e.constructor !== RangeError) { throw e; }
|
|
}
|
|
}
|
|
},
|
|
|
|
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, node);
|
|
}
|
|
},
|
|
|
|
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'() {
|
|
stack.pop();
|
|
},
|
|
|
|
'Program:exit'() {
|
|
classExpressions.forEach((node) => {
|
|
if (isSuperTypeParameterPropsDeclaration(node)) {
|
|
markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node), node);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
};
|