UNPKG

eslint-plugin-sonarjs

Version:
329 lines (328 loc) • 13.8 kB
"use strict"; 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; }