eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
1,498 lines (1,397 loc) • 48.3 kB
JavaScript
/**
* @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')
) {
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
* @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([
'forwardRef',
'ForwardRefRenderFunction',
'VFC',
'VoidFunctionComponent',
'PropsWithChildren',
'SFC',
'StatelessComponent',
'FunctionComponent',
'FC',
]);
const genericTypeParamIndexWherePropsArePresent = {
ForwardRefRenderFunction: 1,
forwardRef: 1,
VoidFunctionComponent: 0,
VFC: 0,
PropsWithChildren: 0,
SFC: 0,
StatelessComponent: 0,
FunctionComponent: 0,
FC: 0,
};
const genericReblendTypesImport = new Set();
// import { FC as X } from 'reblend' -> 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-reblend-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 = Boolean(
annotation.indexers && annotation.indexers.length
);
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 Reblend 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 Reblend 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 buildReblendDeclarationTypes(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 &&
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 (
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':
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 = buildReblendDeclarationTypes(
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 = buildReblendDeclarationTypes(
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) {
// Invalid proptype or cannot analyse statically
return {};
}
/** @type {UnionTypeDefinition} */
const unionTypeDefinition = {
type: 'union',
children: argument.elements.map(element =>
buildReblendDeclarationTypes(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 isValidReblendGenericTypeAnnotation(annotation) {
if (annotation.typeName) {
if (annotation.typeName.name) {
// if FC<Props>
const typeName = annotation.typeName.name;
if (!genericReblendTypesImport.has(typeName)) {
return false;
}
} else if (annotation.typeName.right.name) {
// if Reblend.FC<Props>
const right = annotation.typeName.right.name;
const left = annotation.typeName.left.name;
if (
!genericReblendTypesImport.has(left) ||
!allowedGenericTypes.has(right)
) {
return false;
}
}
}
return true;
}
/**
* Returns the left most typeName of a node, e.g: FC<Props>, Reblend.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 =
genericReblendTypesImport.has(leftMostName);
const nodeTypeParams = node.typeParameters;
if (
shouldTraverseTypeParams &&
nodeTypeParams &&
nodeTypeParams.length !== 0
) {
// All reblend Generic types are derived from:
// type PropsWithChildren<P> = P & { children?: ReblendNode | undefined }
// So we should construct an optional children prop
this.shouldSpecifyOptionalChildrenProps = true;
const rightMostName = getRightMostTypeName(node.typeName);
const importedName = localToImportedMap[rightMostName];
const idx =
genericTypeParamIndexWherePropsArePresent[
leftMostName !== rightMostName ? rightMostName : importedName
];
const nextNode = nodeTypeParams.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 nodeTypeParams = node.typeParameters;
if (nodeTypeParams) {
if (nodeTypeParams.params.length === 1) {
let returnType = nodeTypeParams.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 &&
propNode.argument &&
propNode.argument.type === 'CallExpression'
) {
const propNodeTypeParams =
propNode.argument.typeParameters;
if (propNodeTypeParams) {
this.visitTSNode(propNodeTypeParams);
} 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 = buildReblendDeclarationTypes(
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 (res.typeParameters) {
this.visitTSNode(res.typeParameters);
} 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,
};
}
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 = buildReblendDeclarationTypes(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 = buildReblendDeclarationTypes(
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
.variablesInScope(context, node)
.find(variableInScope => variableInScope.name === 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 propTypeParams = propTypes.typeParameters;
ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(
propTypeParams.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;
}
if (
node.parent &&
node.parent.callee &&
node.parent.typeParameters &&
node.parent.typeParameters.params &&
(node.parent.callee.name === 'forwardRef' ||
(node.parent.callee.object &&
node.parent.callee.property &&
node.parent.callee.object.name === 'Reblend' &&
node.parent.callee.property.name === 'forwardRef'))
) {
const propTypesParams = node.parent.typeParameters;
const declaredPropTypes = {};
const obj = new DeclarePropTypesForTSTypeAnnotation(
propTypesParams.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/scyberLink/create-reblend-app/tree/master/packages/eslint-plugin-reblend/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/scyberLink/create-reblend-app/tree/master/packages/eslint-plugin-reblend/issues/2777#issuecomment-683944481
const annotation = siblingIdentifier.typeAnnotation.typeAnnotation;
if (
annotation &&
annotation.type !== 'TSTypeReference' &&
annotation.typeParameters == null
) {
return;
}
if (!isValidReblendGenericTypeAnnotation(annotation)) return;
markPropTypesAsDeclared(
node,
resolveTypeAnnotation(siblingIdentifier),
rootNode
);
}
}
/**
* 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 = 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' || 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 'reblend`
if (node.source.value === 'reblend') {
node.specifiers.forEach(specifier => {
if (
// handles import * as X from 'reblend'
specifier.type === 'ImportNamespaceSpecifier' ||
// handles import Reblend from 'reblend'
specifier.type === 'ImportDefaultSpecifier'
) {
genericReblendTypesImport.add(specifier.local.name);
}
// handles import { FC } from 'reblend' or import { FC as X } from 'reblend'
if (
specifier.type === 'ImportSpecifier' &&
allowedGenericTypes.has(specifier.imported.name)
) {
genericReblendTypesImport.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
);
}
});
},
};
};