sicua
Version:
A tool for analyzing project structure and dependencies
161 lines (160 loc) • 8.03 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateMaintainabilityIndex = calculateMaintainabilityIndex;
const fileReader_1 = require("../utils/fileReader");
const codeMetrics_1 = require("../utils/codeMetrics");
const astUtils_1 = require("../../translation/utils/astUtils");
const lineCounter_1 = require("../../general/utils/lineCounter");
const reactSpecific_1 = require("../../../utils/ast/reactSpecific");
const typescript_1 = __importDefault(require("typescript"));
const analysisUtils_1 = require("../../../utils/common/analysisUtils");
async function calculateMaintainabilityIndex(components) {
const maintainabilityIndex = {};
// Group components by file path for efficient processing
const componentsByFile = new Map();
components.forEach((component) => {
if (!componentsByFile.has(component.fullPath)) {
componentsByFile.set(component.fullPath, []);
}
componentsByFile.get(component.fullPath).push(component);
});
// Process each file once and calculate maintainability for each component
for (const [filePath, fileComponents] of componentsByFile) {
const content = await (0, fileReader_1.readFileContent)(filePath);
const sourceFile = (0, astUtils_1.createSourceFile)(filePath, content);
// Find all component nodes in the file
const componentNodes = findComponentNodes(sourceFile, fileComponents);
// Calculate maintainability for each component
fileComponents.forEach((component) => {
const componentId = (0, analysisUtils_1.generateComponentId)(component);
const componentNode = componentNodes.get(component.name);
if (componentNode) {
const cyclomaticComplexity = (0, codeMetrics_1.calculateFunctionComplexity)(componentNode);
const halsteadMetrics = (0, codeMetrics_1.calculateHalsteadMetrics)(componentNode);
// Calculate lines of code for this specific component
const componentText = componentNode.getFullText();
const linesOfCode = (0, lineCounter_1.countLines)(componentText).codeLines;
// Calculate Halstead volume with enhanced safety checks
const vocabularySize = halsteadMetrics.n1 + halsteadMetrics.n2;
const programLength = halsteadMetrics.N1 + halsteadMetrics.N2;
const halsteadVolume = vocabularySize > 0 ? programLength * Math.log2(vocabularySize) : 1;
// Calculate base maintainability index using enhanced metrics
let baseIndex = (0, codeMetrics_1.computeMaintainabilityIndex)(halsteadVolume, cyclomaticComplexity, linesOfCode);
// Apply additional adjustments for more accurate assessment
const adjustedIndex = applyMaintainabilityAdjustments(baseIndex, halsteadMetrics, cyclomaticComplexity, linesOfCode, component);
maintainabilityIndex[componentId] = adjustedIndex;
}
else {
// Fallback: if we can't find the specific component, use default values
maintainabilityIndex[componentId] = 50; // Neutral maintainability score
}
});
}
return maintainabilityIndex;
}
/**
* Find component nodes in the source file
*/
function findComponentNodes(sourceFile, components) {
const componentNodes = new Map();
const componentNames = new Set(components.map((c) => c.name));
function 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;
}
function applyMaintainabilityAdjustments(baseIndex, halsteadMetrics, cyclomaticComplexity, linesOfCode, component) {
let adjustedIndex = baseIndex;
// Factor 1: Code repetition penalty
const totalElements = halsteadMetrics.N1 + halsteadMetrics.N2;
const vocabularySize = halsteadMetrics.n1 + halsteadMetrics.n2;
if (vocabularySize > 0) {
const repetitionRatio = totalElements / vocabularySize;
if (repetitionRatio > 10) {
adjustedIndex -= (repetitionRatio - 10) * 0.3;
}
}
// Factor 2: Component complexity factors
if (component.functions && component.functions.length > 20) {
adjustedIndex -= (component.functions.length - 20) * 0.2;
}
// Factor 3: High coupling penalty
const totalConnections = component.imports.length + component.usedBy.length;
if (totalConnections > 15) {
adjustedIndex -= (totalConnections - 15) * 0.1;
}
// Factor 4: Props complexity (if available)
if (component.props && component.props.length > 10) {
const requiredPropsCount = component.props.filter((p) => p.required).length;
if (requiredPropsCount > 5) {
adjustedIndex -= (requiredPropsCount - 5) * 0.3;
}
}
// Factor 5: File size penalties (adjusted for component-specific lines)
if (linesOfCode > 200) {
// Lower threshold since this is per-component
adjustedIndex -= Math.min((linesOfCode - 200) * 0.01, 5);
}
// Factor 6: Very high complexity penalty
if (cyclomaticComplexity > 15) {
adjustedIndex -= (cyclomaticComplexity - 15) * 0.4;
}
// Factor 7: Directory-based adjustments
if (component.directory.includes("shared") ||
component.directory.includes("common") ||
component.directory.includes("utils")) {
// Shared components should be more maintainable
if (adjustedIndex < 70) {
adjustedIndex -= 2; // Penalty for low maintainability in shared code
}
}
// Factor 8: Function call complexity
if (component.functionCalls) {
const totalFunctionCalls = Object.values(component.functionCalls).reduce((sum, calls) => sum + calls.length, 0);
if (totalFunctionCalls > 30) {
adjustedIndex -= (totalFunctionCalls - 30) * 0.05;
}
}
// Factor 9: Export complexity
if (component.exports.length > 5) {
adjustedIndex -= (component.exports.length - 5) * 0.2;
}
// Factor 10: High usage component penalty (harder to maintain when many depend on it)
if (component.usedBy.length > 10) {
adjustedIndex -= (component.usedBy.length - 10) * 0.1;
}
// Ensure bounds and reasonable precision
return Math.max(0, Math.min(100, Math.round(adjustedIndex * 100) / 100));
}