sicua
Version:
A tool for analyzing project structure and dependencies
635 lines (634 loc) • 31.1 kB
JavaScript
;
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;