UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

635 lines (634 loc) 31.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedComplexityCalculator = void 0; const typescript_1 = __importDefault(require("typescript")); const reactSpecific_1 = require("../../../utils/ast/reactSpecific"); const analysisUtils_1 = require("../../../utils/common/analysisUtils"); const lineCounter_1 = require("../../general/utils/lineCounter"); const path_1 = __importDefault(require("path")); const ComponentFilter_1 = require("../../seo/utils/ComponentFilter"); /** * Unified complexity calculator that processes each file once and calculates all metrics * Uses existing parsed source files and optimized lookup services */ class UnifiedComplexityCalculator { constructor(scanResult) { this.scanResult = scanResult; } /** * Calculate all complexity metrics in a single pass with component filtering */ calculateAllMetrics(components) { // Filter to only include actual React components const actualComponents = ComponentFilter_1.ComponentFilter.filterComponents(components); const result = { componentComplexity: {}, couplingDegree: {}, cyclomaticComplexity: {}, maintainabilityIndex: {}, cognitiveComplexity: {}, }; // Calculate component-level metrics (no AST traversal needed) this.calculateComponentComplexity(actualComponents, result.componentComplexity); this.calculateCouplingDegree(actualComponents, result.couplingDegree); // Group components by file path for efficient AST-based processing const componentsByFile = this.groupComponentsByFile(actualComponents); // Process each file once, calculating all AST-based metrics for (const [filePath, fileComponents] of componentsByFile) { this.processFileMetrics(filePath, fileComponents, result); } return result; } // Rest of the methods remain the same but work with filtered components groupComponentsByFile(components) { const componentsByFile = new Map(); for (const component of components) { if (!componentsByFile.has(component.fullPath)) { componentsByFile.set(component.fullPath, []); } componentsByFile.get(component.fullPath).push(component); } return componentsByFile; } /** * Process all AST-based metrics for a single file */ processFileMetrics(filePath, fileComponents, result) { // Use existing parsed source file from scan result const sourceFile = this.scanResult.sourceFiles.get(filePath); if (!sourceFile) { // Fallback values for components without source files for (const component of fileComponents) { const componentId = (0, analysisUtils_1.generateComponentId)(component); result.cyclomaticComplexity[componentId] = 1; result.cognitiveComplexity[componentId] = 0; result.maintainabilityIndex[componentId] = 50; } return; } // Find all component nodes in the file in a single traversal const componentNodes = this.findComponentNodes(sourceFile, fileComponents); // Calculate metrics for each component for (const component of fileComponents) { const componentId = (0, analysisUtils_1.generateComponentId)(component); const componentNode = componentNodes.get(component.name); if (componentNode) { const metrics = this.calculateNodeMetrics(componentNode, component); result.cyclomaticComplexity[componentId] = metrics.cyclomaticComplexity; result.cognitiveComplexity[componentId] = metrics.cognitiveComplexity; result.maintainabilityIndex[componentId] = metrics.maintainabilityIndex; } else { // Fallback values result.cyclomaticComplexity[componentId] = 1; result.cognitiveComplexity[componentId] = 0; result.maintainabilityIndex[componentId] = 50; } } } /** * Find component nodes in source file (single traversal) */ findComponentNodes(sourceFile, components) { const componentNodes = new Map(); const componentNames = new Set(components.map((c) => c.name)); const visit = (node) => { // Check for function declarations if (typescript_1.default.isFunctionDeclaration(node) && node.name) { const functionName = node.name.text; if (componentNames.has(functionName) && (0, reactSpecific_1.isReactComponent)(node)) { componentNodes.set(functionName, node); } } // Check for variable declarations (const ComponentName = ...) if (typescript_1.default.isVariableDeclaration(node) && typescript_1.default.isIdentifier(node.name)) { const varName = node.name.text; if (componentNames.has(varName) && node.initializer) { if ((typescript_1.default.isArrowFunction(node.initializer) || typescript_1.default.isFunctionExpression(node.initializer)) && (0, reactSpecific_1.isReactComponent)(node.initializer)) { componentNodes.set(varName, node.initializer); } } } // Check for exported function declarations if (typescript_1.default.isExportAssignment(node) && typescript_1.default.isFunctionDeclaration(node.expression)) { const func = node.expression; if (func.name && componentNames.has(func.name.text) && (0, reactSpecific_1.isReactComponent)(func)) { componentNodes.set(func.name.text, func); } } typescript_1.default.forEachChild(node, visit); }; visit(sourceFile); return componentNodes; } /** * Calculate all metrics for a single component node */ calculateNodeMetrics(node, component) { const cyclomaticComplexity = this.calculateCyclomaticComplexity(node); const cognitiveComplexity = this.calculateCognitiveComplexity(node); const maintainabilityIndex = this.calculateMaintainabilityIndex(node, component, cyclomaticComplexity); return { cyclomaticComplexity, cognitiveComplexity, maintainabilityIndex, componentComplexity: 0, // Calculated separately couplingDegree: 0, // Calculated separately }; } /** * Calculate cyclomatic complexity for a node */ calculateCyclomaticComplexity(node) { let complexity = 1; // Base complexity const incrementComplexity = (currentNode) => { switch (currentNode.kind) { case typescript_1.default.SyntaxKind.IfStatement: case typescript_1.default.SyntaxKind.ConditionalExpression: case typescript_1.default.SyntaxKind.ForStatement: case typescript_1.default.SyntaxKind.ForInStatement: case typescript_1.default.SyntaxKind.ForOfStatement: case typescript_1.default.SyntaxKind.WhileStatement: case typescript_1.default.SyntaxKind.DoStatement: case typescript_1.default.SyntaxKind.CaseClause: case typescript_1.default.SyntaxKind.CatchClause: complexity++; break; case typescript_1.default.SyntaxKind.BinaryExpression: const binaryExpr = currentNode; if (binaryExpr.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken || binaryExpr.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) { complexity++; } break; case typescript_1.default.SyntaxKind.QuestionDotToken: case typescript_1.default.SyntaxKind.QuestionQuestionToken: complexity++; break; case typescript_1.default.SyntaxKind.JsxExpression: const jsxExpression = currentNode; if (jsxExpression.expression) { if (typescript_1.default.isBinaryExpression(jsxExpression.expression) && jsxExpression.expression.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken) { complexity++; } else if (typescript_1.default.isConditionalExpression(jsxExpression.expression)) { complexity++; } } break; case typescript_1.default.SyntaxKind.FunctionExpression: case typescript_1.default.SyntaxKind.ArrowFunction: case typescript_1.default.SyntaxKind.FunctionDeclaration: case typescript_1.default.SyntaxKind.MethodDeclaration: if (currentNode !== node) { return; // Don't traverse nested functions } break; } typescript_1.default.forEachChild(currentNode, incrementComplexity); }; typescript_1.default.forEachChild(node, incrementComplexity); return complexity; } /** * Calculate cognitive complexity for a node */ calculateCognitiveComplexity(node) { let totalComplexity = 0; const calculateComplexity = (currentNode, nestingLevel = 0) => { let nodeComplexity = 0; switch (currentNode.kind) { case typescript_1.default.SyntaxKind.IfStatement: nodeComplexity += 1 + nestingLevel; const ifStatement = currentNode; nodeComplexity += calculateComplexity(ifStatement.expression, nestingLevel); nodeComplexity += calculateComplexity(ifStatement.thenStatement, nestingLevel + 1); if (ifStatement.elseStatement) { if (typescript_1.default.isIfStatement(ifStatement.elseStatement)) { nodeComplexity += calculateComplexity(ifStatement.elseStatement, nestingLevel); } else { nodeComplexity += calculateComplexity(ifStatement.elseStatement, nestingLevel + 1); } } break; case typescript_1.default.SyntaxKind.ForStatement: case typescript_1.default.SyntaxKind.ForInStatement: case typescript_1.default.SyntaxKind.ForOfStatement: case typescript_1.default.SyntaxKind.WhileStatement: case typescript_1.default.SyntaxKind.DoStatement: nodeComplexity += 1 + nestingLevel; typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, nestingLevel + 1); }); break; case typescript_1.default.SyntaxKind.SwitchStatement: nodeComplexity += 1 + nestingLevel; const switchStatement = currentNode; nodeComplexity += calculateComplexity(switchStatement.expression, nestingLevel); switchStatement.caseBlock.clauses.forEach((clause) => { if (typescript_1.default.isCaseClause(clause)) { nodeComplexity += 1 + nestingLevel; } clause.statements.forEach((statement) => { nodeComplexity += calculateComplexity(statement, nestingLevel + 1); }); }); break; case typescript_1.default.SyntaxKind.TryStatement: const tryStatement = currentNode; nodeComplexity += calculateComplexity(tryStatement.tryBlock, nestingLevel); if (tryStatement.catchClause) { nodeComplexity += 1 + nestingLevel; nodeComplexity += calculateComplexity(tryStatement.catchClause.block, nestingLevel + 1); } if (tryStatement.finallyBlock) { nodeComplexity += calculateComplexity(tryStatement.finallyBlock, nestingLevel); } break; case typescript_1.default.SyntaxKind.ConditionalExpression: nodeComplexity += 1 + nestingLevel; const conditionalExpression = currentNode; nodeComplexity += calculateComplexity(conditionalExpression.condition, nestingLevel); nodeComplexity += calculateComplexity(conditionalExpression.whenTrue, nestingLevel + 1); nodeComplexity += calculateComplexity(conditionalExpression.whenFalse, nestingLevel + 1); break; case typescript_1.default.SyntaxKind.BinaryExpression: const binaryExpression = currentNode; if (binaryExpression.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken || binaryExpression.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) { nodeComplexity += 1 + nestingLevel; } nodeComplexity += calculateComplexity(binaryExpression.left, nestingLevel); nodeComplexity += calculateComplexity(binaryExpression.right, nestingLevel); break; case typescript_1.default.SyntaxKind.FunctionExpression: case typescript_1.default.SyntaxKind.ArrowFunction: typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, 0); }); break; case typescript_1.default.SyntaxKind.FunctionDeclaration: case typescript_1.default.SyntaxKind.MethodDeclaration: if (currentNode !== node) { typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, 0); }); } else { typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, nestingLevel); }); } break; case typescript_1.default.SyntaxKind.JsxExpression: const jsxExpression = currentNode; if (jsxExpression.expression) { if (typescript_1.default.isBinaryExpression(jsxExpression.expression) && jsxExpression.expression.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken) { nodeComplexity += 1 + nestingLevel; } else if (typescript_1.default.isConditionalExpression(jsxExpression.expression)) { nodeComplexity += 1 + nestingLevel; } nodeComplexity += calculateComplexity(jsxExpression.expression, nestingLevel); } break; case typescript_1.default.SyntaxKind.QuestionQuestionToken: nodeComplexity += 1; typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, nestingLevel); }); break; default: typescript_1.default.forEachChild(currentNode, (child) => { nodeComplexity += calculateComplexity(child, nestingLevel); }); } return nodeComplexity; }; totalComplexity = calculateComplexity(node); return totalComplexity; } /** * Calculate maintainability index using the Microsoft normalized formula: * MI = MAX(0, (171 - 5.2 * ln(HV) - 0.23 * CC - 16.2 * ln(LOC)) * 100 / 171) * Range: 0-100 where higher is better */ calculateMaintainabilityIndex(node, component, cyclomaticComplexity) { const halsteadMetrics = this.calculateHalsteadMetrics(node); const componentText = node.getFullText(); const linesOfCode = (0, lineCounter_1.countLines)(componentText).codeLines; // Calculate Halstead Volume const vocabularySize = halsteadMetrics.n1 + halsteadMetrics.n2; const programLength = halsteadMetrics.N1 + halsteadMetrics.N2; let halsteadVolume = 1; // Default to 1 to avoid log(0) if (vocabularySize > 0 && programLength > 0) { halsteadVolume = programLength * Math.log2(vocabularySize); } // Ensure we have positive values for logarithms const safeHalsteadVolume = Math.max(halsteadVolume, 1); const safeLinesOfCode = Math.max(linesOfCode, 1); // Apply the Microsoft maintainability index formula const rawMaintainabilityIndex = 171 - 5.2 * Math.log(safeHalsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(safeLinesOfCode); // Normalize to 0-100 range as per Microsoft's specification const normalizedIndex = Math.max(0, (rawMaintainabilityIndex * 100) / 171); // Apply maintainability adjustments based on component characteristics const adjustedIndex = this.applyMaintainabilityAdjustments(normalizedIndex, halsteadMetrics, component); return Math.round(adjustedIndex * 100) / 100; } /** * Apply maintainability adjustments based on component characteristics * Adjustments are applied to the normalized 0-100 scale */ applyMaintainabilityAdjustments(baseIndex, halsteadMetrics, component) { let adjustedIndex = baseIndex; // Code repetition penalty (adjusted for 0-100 scale) const totalElements = halsteadMetrics.N1 + halsteadMetrics.N2; const vocabularySize = halsteadMetrics.n1 + halsteadMetrics.n2; if (vocabularySize > 0) { const repetitionRatio = totalElements / vocabularySize; if (repetitionRatio > 10) { // Reduce by up to 15 points for high repetition adjustedIndex -= Math.min((repetitionRatio - 10) * 1.5, 15); } } // Component complexity factors (adjusted for 0-100 scale) if (component.functions && component.functions.length > 20) { // Reduce by up to 10 points for too many functions adjustedIndex -= Math.min((component.functions.length - 20) * 0.5, 10); } // High coupling penalty (adjusted for 0-100 scale) const totalConnections = component.imports.length + component.usedBy.length; if (totalConnections > 15) { // Reduce by up to 8 points for high coupling adjustedIndex -= Math.min((totalConnections - 15) * 0.4, 8); } // Ensure we stay within 0-100 bounds return Math.max(0, Math.min(100, adjustedIndex)); } /** * Calculate Halstead metrics for a node */ calculateHalsteadMetrics(node) { const operators = new Set(); const operands = new Set(); let totalOperators = 0; let totalOperands = 0; const visitor = (node) => { switch (node.kind) { case typescript_1.default.SyntaxKind.BinaryExpression: const binaryExpr = node; operators.add(binaryExpr.operatorToken.getText()); totalOperators++; break; case typescript_1.default.SyntaxKind.PrefixUnaryExpression: case typescript_1.default.SyntaxKind.PostfixUnaryExpression: const unaryExpr = node; operators.add(typescript_1.default.tokenToString(unaryExpr.operator) || unaryExpr.operator.toString()); totalOperators++; break; case typescript_1.default.SyntaxKind.CallExpression: const callExpr = node; if (typescript_1.default.isIdentifier(callExpr.expression)) { operators.add(callExpr.expression.text); totalOperators++; } else if (typescript_1.default.isPropertyAccessExpression(callExpr.expression)) { operators.add(callExpr.expression.name.text); totalOperators++; } break; case typescript_1.default.SyntaxKind.Identifier: const identifier = node; const parent = identifier.parent; if (!typescript_1.default.isVariableDeclaration(parent) || parent.name !== identifier) { operands.add(identifier.text); totalOperands++; } break; case typescript_1.default.SyntaxKind.StringLiteral: case typescript_1.default.SyntaxKind.NumericLiteral: case typescript_1.default.SyntaxKind.TrueKeyword: case typescript_1.default.SyntaxKind.FalseKeyword: case typescript_1.default.SyntaxKind.NullKeyword: case typescript_1.default.SyntaxKind.UndefinedKeyword: operands.add(node.getText()); totalOperands++; break; } typescript_1.default.forEachChild(node, visitor); }; visitor(node); return { n1: operators.size, n2: operands.size, N1: totalOperators, N2: totalOperands, }; } /** * Compute maintainability index using standard formula */ computeMaintainabilityIndex(halsteadVolume, cyclomaticComplexity, linesOfCode) { if (linesOfCode === 0) return 100; if (halsteadVolume <= 0) halsteadVolume = 1; if (cyclomaticComplexity <= 0) cyclomaticComplexity = 1; let maintainabilityIndex = 171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(linesOfCode); maintainabilityIndex = (maintainabilityIndex * 100) / 171; return Math.max(0, Math.min(100, Math.round(maintainabilityIndex * 100) / 100)); } /** * Calculate component complexity (non-AST based) */ calculateComponentComplexity(components, result) { for (const component of components) { let componentComplexity = 0; componentComplexity += component.imports.length + component.usedBy.length; componentComplexity += component.exports.length * 0.5; if (component.functions) { componentComplexity += component.functions.length * 1.5; } if (component.functionCalls) { const totalFunctionCalls = Object.values(component.functionCalls).reduce((sum, calls) => sum + calls.length, 0); componentComplexity += totalFunctionCalls * 0.3; } if (component.props) { const requiredPropsComplexity = component.props.filter((prop) => prop.required).length * 1.2; const optionalPropsComplexity = component.props.filter((prop) => !prop.required).length * 0.8; componentComplexity += requiredPropsComplexity + optionalPropsComplexity; } if (component.usedBy.length > 5) { componentComplexity *= 1 + (component.usedBy.length - 5) * 0.1; } const componentId = (0, analysisUtils_1.generateComponentId)(component); result[componentId] = Math.round(componentComplexity * 10) / 10; } } /** * Calculate coupling degree with proper normalization */ /** * Calculate coupling degree using the proper software engineering formula: * C = 1 - 1/(di + 2×ci + do + 2×co + gd + 2×gc + w + r) * Range: ~0.67 (low coupling) to 1.0 (highly coupled) */ calculateCouplingDegree(components, result) { // Create a map for efficient lookups const componentMap = new Map(); components.forEach((comp) => { componentMap.set(comp.name, comp); }); for (const component of components) { const componentId = (0, analysisUtils_1.generateComponentId)(component); // Fan-in: number of modules calling this module const r = component.usedBy.length; // Fan-out: number of modules this module calls (only count actual components) const w = component.imports.filter((imp) => { const importName = path_1.default.basename(imp, path_1.default.extname(imp)); return componentMap.has(importName); }).length; // Input parameters analysis from props let di = 0; // input data parameters let ci = 0; // input control parameters if (component.props) { for (const prop of component.props) { if (this.isControlParameter(prop)) { ci++; } else { di++; } } } // Output parameters analysis from exports let do_param = 0; // output data parameters let co = 0; // output control parameters if (component.exports) { for (const exportName of component.exports) { if (this.isControlExport(exportName, component)) { co++; } else { do_param++; } } } // Global coupling analysis const globalAnalysis = this.analyzeGlobalCoupling(component); const gd = globalAnalysis.dataGlobals; // global variables used as data const gc = globalAnalysis.controlGlobals; // global variables used as control // Apply the coupling formula const denominator = di + 2 * ci + do_param + 2 * co + gd + 2 * gc + w + r; let coupling = 0; if (denominator > 0) { coupling = 1 - 1 / denominator; } // Ensure minimum coupling value (components with very few connections) coupling = Math.max(coupling, 0); result[componentId] = Math.round(coupling * 100) / 100; } } /** * Determine if a prop is a control parameter (callback, handler, function) */ isControlParameter(prop) { const controlPatterns = [ /^on[A-Z]/, // onClick, onSubmit, etc. /^handle[A-Z]/, // handleClick, handleSubmit, etc. /Handler$/, // clickHandler, submitHandler, etc. /Callback$/, // onCallback, submitCallback, etc. /Function$/, // renderFunction, etc. ]; const isControlName = controlPatterns.some((pattern) => pattern.test(prop.name)); const isControlType = prop.type.includes("function") || prop.type.includes("=>") || prop.type.includes("()") || prop.type.includes("Function"); return isControlName || isControlType; } /** * Determine if an export is a control export (function, handler) */ isControlExport(exportName, component) { const controlPatterns = [ /^handle[A-Z]/, // handleClick, handleSubmit, etc. /^on[A-Z]/, // onClick, onSubmit, etc. /Handler$/, // clickHandler, submitHandler, etc. /Function$/, // renderFunction, etc. /^use[A-Z]/, // custom hooks ]; const isControlName = controlPatterns.some((pattern) => pattern.test(exportName)); // Check if it's in the functions array (indicating it's a function export) const isFunction = component.functions?.includes(exportName) || false; return isControlName || isFunction; } /** * Analyze global coupling by examining content for global variable usage */ analyzeGlobalCoupling(component) { if (!component.content) { return { dataGlobals: 0, controlGlobals: 0 }; } let dataGlobals = 0; let controlGlobals = 0; // Global data patterns const globalDataPatterns = [ /process\.env\./g, // environment variables /window\./g, // window object access /document\./g, // document object access /localStorage\./g, // localStorage access /sessionStorage\./g, // sessionStorage access /global\./g, // explicit global access ]; // Global control patterns const globalControlPatterns = [ /window\.location/g, // navigation control /history\./g, // history manipulation /router\./g, // router control /dispatch\(/g, // state dispatch /\.push\(/g, // navigation push /\.replace\(/g, // navigation replace ]; // Count global data usage globalDataPatterns.forEach((pattern) => { const matches = component.content.match(pattern); if (matches) { dataGlobals += matches.length; } }); // Count global control usage globalControlPatterns.forEach((pattern) => { const matches = component.content.match(pattern); if (matches) { controlGlobals += matches.length; } }); return { dataGlobals, controlGlobals }; } } exports.UnifiedComplexityCalculator = UnifiedComplexityCalculator;