eslint-plugin-sonarjs
Version:
329 lines (328 loc) • 13.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isForwardRefCallee = isForwardRefCallee;
exports.getComponentPropsType = getComponentPropsType;
exports.isBuiltinReactSuperclass = isBuiltinReactSuperclass;
exports.isReactClassComponent = isReactClassComponent;
exports.isComponentNode = isComponentNode;
exports.createComponentAnalysis = createComponentAnalysis;
exports.collectComponents = collectComponents;
exports.collectComponentNodes = collectComponentNodes;
exports.getComponentIdentifier = getComponentIdentifier;
const typescript_1 = __importDefault(require("typescript"));
const ancestor_js_1 = require("../ancestor.js");
const ast_js_1 = require("../ast.js");
const type_js_1 = require("../type.js");
const COMPONENT_NODE_TYPES = new Set([
'ClassDeclaration',
'ClassExpression',
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
]);
const REACT_LOCAL_CLASS_SUPERS = new Set(['Component', 'PureComponent']);
const REACT_FUNCTION_COMPONENT_TYPES = new Set(['FC', 'FunctionComponent']);
const REACT_FORWARD_REF_RENDER_FUNCTION_TYPES = new Set(['ForwardRefRenderFunction']);
const REACT_COMPONENT_WRAPPER_CALLEES = new Set(['memo', 'forwardRef']);
const EMPTY_COMPONENT_ANALYSIS = {
memberPropsTypeCandidates: [],
enclosingTypePropsTypeCandidates: [],
};
function isClassComponentNode(node) {
return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
}
function isFunctionComponentNode(node) {
return (node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression');
}
function hasIdentifierId(node) {
return 'id' in node && node.id != null && (0, ast_js_1.isIdentifier)(node.id);
}
function getClassComponentTsNode(componentNode, services) {
return services.esTreeNodeToTSNodeMap.get(componentNode);
}
function isVariableDeclaratorWithIdentifierId(node) {
return (!!node &&
typeof node === 'object' &&
'type' in node &&
node.type === 'VariableDeclarator' &&
'id' in node &&
!!node.id &&
typeof node.id === 'object' &&
'type' in node.id &&
node.id.type === 'Identifier');
}
function isVariableAssignedFunctionOrClassExpression(componentNode, parent) {
return ((componentNode.type === 'ClassExpression' || componentNode.type === 'FunctionExpression') &&
isVariableDeclaratorWithIdentifierId(parent));
}
function isReactComponentWrapperCallee(callee) {
return ((callee.type === 'Identifier' && REACT_COMPONENT_WRAPPER_CALLEES.has(callee.name)) ||
(callee.type === 'MemberExpression' &&
(0, ast_js_1.isIdentifier)(callee.object, 'React') &&
callee.property.type === 'Identifier' &&
REACT_COMPONENT_WRAPPER_CALLEES.has(callee.property.name)));
}
/**
* Returns true when `callee` is a call to `forwardRef` or `React.forwardRef`.
*/
function isForwardRefCallee(callee) {
return ((0, ast_js_1.isIdentifier)(callee, 'forwardRef') ||
(callee.type === 'MemberExpression' &&
(0, ast_js_1.isIdentifier)(callee.object, 'React') &&
(0, ast_js_1.isIdentifier)(callee.property, 'forwardRef')));
}
function isWrappedInReactComponentCall(node, parent) {
return (parent?.type === 'CallExpression' &&
parent.arguments.includes(node) &&
isReactComponentWrapperCallee(parent.callee));
}
function getOutermostReactWrapperParent(componentNode) {
let currentNode = componentNode;
let parent = (0, ancestor_js_1.getNodeParent)(currentNode);
while (isWrappedInReactComponentCall(currentNode, parent)) {
currentNode = parent;
parent = (0, ancestor_js_1.getNodeParent)(currentNode);
}
return parent;
}
function getOwningVariableDeclarator(componentNode) {
const parent = getOutermostReactWrapperParent(componentNode);
return isVariableDeclaratorWithIdentifierId(parent) ? parent : undefined;
}
function isAnonymousDefaultExportComponent(componentNode) {
const parent = getOutermostReactWrapperParent(componentNode);
return parent?.type === 'ExportDefaultDeclaration';
}
function getComponentPropsType(componentNode, services) {
const checker = services.program.getTypeChecker();
if (isFunctionComponentNode(componentNode)) {
return (getFunctionComponentParamPropsType(componentNode, services) ??
getComponentPropsTypeFromVariableDeclaration(componentNode, services));
}
if (!isClassComponentNode(componentNode)) {
return undefined;
}
const tsNode = getClassComponentTsNode(componentNode, services);
return getDeclaredClassPropsType(tsNode, checker) ?? getClassPropsPropertyType(tsNode, checker);
}
function getComponentPropsTypeCandidates(componentNode, services) {
if (!isFunctionComponentNode(componentNode)) {
const propsType = getComponentPropsType(componentNode, services);
return propsType ? [propsType] : [];
}
const checker = services.program.getTypeChecker();
const propsTypes = [];
const primaryPropsType = getFunctionComponentParamPropsType(componentNode, services);
if (primaryPropsType) {
propsTypes.push(primaryPropsType);
}
const declaredVariablePropsType = getComponentPropsTypeFromVariableDeclaration(componentNode, services);
if (declaredVariablePropsType &&
!(primaryPropsType &&
(0, type_js_1.areSameTypeDeclarations)(checker, primaryPropsType, declaredVariablePropsType))) {
propsTypes.push(declaredVariablePropsType);
}
return propsTypes;
}
function isSpecificPropsType(type) {
return !!type && (type.flags & (typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) === 0;
}
function getFunctionComponentParamPropsType(componentNode, services) {
const firstParam = componentNode.params[0];
const propsType = firstParam ? (0, type_js_1.getTypeFromTreeNode)(firstParam, services) : undefined;
return isSpecificPropsType(propsType) ? propsType : undefined;
}
function getComponentPropsTypeFromVariableDeclaration(componentNode, services) {
const variableDeclarator = getOwningVariableDeclarator(componentNode);
if (!variableDeclarator) {
return undefined;
}
const declaredType = variableDeclarator.id.typeAnnotation
?.typeAnnotation;
if (!declaredType) {
return undefined;
}
const propsTypeNode = findDeclaredFunctionComponentPropsType(declaredType);
return propsTypeNode
? (0, type_js_1.getTypeFromTreeNode)(propsTypeNode, services)
: undefined;
}
function findDeclaredFunctionComponentPropsType(typeNode) {
if (typeNode.type === 'TSIntersectionType') {
for (const type of typeNode.types) {
const propsType = findDeclaredFunctionComponentPropsType(type);
if (propsType) {
return propsType;
}
}
return undefined;
}
if (typeNode.type !== 'TSTypeReference') {
return undefined;
}
const typeName = getRightmostTypeName(typeNode.typeName);
const typeArguments = typeNode.typeArguments?.params ?? [];
if (typeName && REACT_FUNCTION_COMPONENT_TYPES.has(typeName)) {
return typeArguments[0];
}
if (typeName && REACT_FORWARD_REF_RENDER_FUNCTION_TYPES.has(typeName)) {
return typeArguments[1];
}
return undefined;
}
function getRightmostTypeName(typeName) {
if (typeName.type === 'Identifier') {
return typeName.name;
}
if (typeName.type === 'TSQualifiedName') {
return typeName.right.name;
}
return undefined;
}
function isReactClassSuperName(name) {
return REACT_LOCAL_CLASS_SUPERS.has(name);
}
function isQualifiedReactClassSuper(objectName, propertyName) {
return objectName === undefined
? isReactClassSuperName(propertyName)
: objectName === 'React' && isReactClassSuperName(propertyName);
}
function isBuiltinReactSuperclass(superClass) {
return ((superClass.type === 'Identifier' && isQualifiedReactClassSuper(undefined, superClass.name)) ||
(superClass.type === 'MemberExpression' &&
(0, ast_js_1.isIdentifier)(superClass.object, 'React') &&
superClass.property.type === 'Identifier' &&
isQualifiedReactClassSuper(superClass.object.name, superClass.property.name)));
}
function isReactClassComponent(node) {
return (isClassComponentNode(node) &&
node.superClass != null &&
isBuiltinReactSuperclass(node.superClass));
}
function isReactComponentHeritageSuperclass(superclass) {
const expression = superclass.expression;
if (typescript_1.default.isIdentifier(expression)) {
return isQualifiedReactClassSuper(undefined, expression.text);
}
return (typescript_1.default.isPropertyAccessExpression(expression) &&
typescript_1.default.isIdentifier(expression.expression) &&
isQualifiedReactClassSuper(expression.expression.text, expression.name.text));
}
function getDeclaredClassPropsType(classNode, checker) {
const extendsClause = classNode.heritageClauses?.find(clause => clause.token === typescript_1.default.SyntaxKind.ExtendsKeyword);
const reactSuperclass = extendsClause?.types.find(type => isReactComponentHeritageSuperclass(type));
const propsTypeNode = reactSuperclass?.typeArguments?.[0];
return propsTypeNode ? checker.getTypeAtLocation(propsTypeNode) : undefined;
}
function getClassPropsPropertyType(classNode, checker) {
if (!classNode.name) {
return undefined;
}
const classSymbol = checker.getSymbolAtLocation(classNode.name);
const propsSymbol = classSymbol
? checker.getDeclaredTypeOfSymbol(classSymbol).getProperty('props')
: undefined;
return propsSymbol ? checker.getTypeOfSymbol(propsSymbol) : undefined;
}
/**
* Returns whether a function component should participate in component analysis.
* Named components must follow React's uppercase convention, while anonymous
* function components stay eligible only when they are exported as default.
*/
function isEligibleFunctionComponent(componentNode, componentIdentifier) {
return componentIdentifier === undefined
? isAnonymousDefaultExportComponent(componentNode)
: /^[A-Z]/.test(componentIdentifier.name);
}
function hasRenderMethodOrProperty(componentNode) {
if (!isClassComponentNode(componentNode)) {
return false;
}
return componentNode.body.body.some(member => (member.type === 'MethodDefinition' || member.type === 'PropertyDefinition') &&
member.key.type === 'Identifier' &&
member.key.name === 'render');
}
function isComponentNode(node) {
return COMPONENT_NODE_TYPES.has(node.type);
}
function isCollectedComponent(component) {
return 'componentNode' in component;
}
function getCollectedComponent(component) {
return isCollectedComponent(component)
? component
: {
componentNode: component,
componentIdentifier: getComponentIdentifier(component),
};
}
function createComponentAnalysis(component, services) {
const { componentNode, componentIdentifier } = getCollectedComponent(component);
if (isClassComponentNode(componentNode)) {
if (!hasRenderMethodOrProperty(componentNode)) {
return EMPTY_COMPONENT_ANALYSIS;
}
const checker = services.program.getTypeChecker();
const classPropsType = getClassPropsPropertyType(getClassComponentTsNode(componentNode, services), checker);
const memberPropsTypeCandidates = getComponentPropsTypeCandidates(componentNode, services);
return {
memberPropsTypeCandidates,
enclosingTypePropsTypeCandidates: classPropsType ? [classPropsType] : [],
};
}
if (!isFunctionComponentNode(componentNode) ||
!isEligibleFunctionComponent(componentNode, componentIdentifier)) {
return EMPTY_COMPONENT_ANALYSIS;
}
const propsTypeCandidates = getComponentPropsTypeCandidates(componentNode, services);
return {
memberPropsTypeCandidates: propsTypeCandidates,
enclosingTypePropsTypeCandidates: propsTypeCandidates,
};
}
function collectComponent(componentNode) {
return {
componentNode,
componentIdentifier: getComponentIdentifier(componentNode),
};
}
function collectComponents(root, keys) {
const result = [];
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (node === undefined) {
continue;
}
if (isComponentNode(node)) {
result.push(collectComponent(node));
continue;
}
const children = (0, ancestor_js_1.childrenOf)(node, keys);
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
}
}
return result;
}
function collectComponentNodes(root, keys) {
return collectComponents(root, keys).map(component => component.componentNode);
}
function getComponentIdentifier(componentNode) {
const variableDeclarator = getOwningVariableDeclarator(componentNode);
if (variableDeclarator &&
(isVariableAssignedFunctionOrClassExpression(componentNode, (0, ancestor_js_1.getNodeParent)(componentNode)) ||
componentNode.type === 'ArrowFunctionExpression' ||
(0, ancestor_js_1.getNodeParent)(componentNode)?.type === 'CallExpression')) {
return variableDeclarator.id;
}
if (hasIdentifierId(componentNode)) {
return componentNode.id;
}
return variableDeclarator?.id;
}