Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
MetricCalculator.ts19.4 kB
// Copyright 2025 Chris Bunting // Brief: Metric Calculator for Code Complexity Analyzer MCP Server // Scope: Calculates various complexity metrics including cyclomatic, cognitive, and Halstead metrics import { ASTNode } from './ASTProcessor.js'; import { Logger } from '../utils/Logger.js'; import { HalsteadMetrics } from '@mcp-code-analysis/shared-types'; export interface ComplexityMetrics { cyclomaticComplexity: number; cognitiveComplexity: number; halsteadMetrics?: HalsteadMetrics; linesOfCode: number; commentLines: number; maintainabilityIndex: number; nestingDepth: number; functionCount: number; classCount: number; averageFunctionSize: number; coupling: number; cohesion: number; } export interface MetricCalculationResult { filePath: string; metrics: ComplexityMetrics; functionMetrics: Map<string, Partial<ComplexityMetrics>>; classMetrics: Map<string, Partial<ComplexityMetrics>>; calculationTime: number; errors: string[]; } export class MetricCalculator { private logger: Logger; constructor() { this.logger = new Logger(); } public async calculateMetrics( ast: ASTNode, filePath: string, sourceCode?: string ): Promise<MetricCalculationResult> { const startTime = Date.now(); const errors: string[] = []; const functionMetrics = new Map<string, Partial<ComplexityMetrics>>(); const classMetrics = new Map<string, Partial<ComplexityMetrics>>(); try { // Calculate basic metrics const linesOfCode = this.calculateLinesOfCode(sourceCode || ''); const commentLines = this.calculateCommentLines(sourceCode || ''); // Calculate function and class counts const functions = this.findFunctions(ast); const classes = this.findClasses(ast); const functionCount = functions.length; const classCount = classes.length; // Calculate average function size const averageFunctionSize = functionCount > 0 ? linesOfCode / functionCount : 0; // Calculate function-level metrics for (const func of functions) { const funcMetrics = this.calculateFunctionMetrics(func, sourceCode || ''); functionMetrics.set(func.name || `anonymous_${func.start.line}`, funcMetrics); } // Calculate class-level metrics for (const cls of classes) { const clsMetrics = this.calculateClassMetrics(cls, sourceCode || ''); classMetrics.set(cls.name || `anonymous_${cls.start.line}`, clsMetrics); } // Calculate overall metrics const cyclomaticComplexity = this.calculateCyclomaticComplexity(ast); const cognitiveComplexity = this.calculateCognitiveComplexity(ast); const halsteadMetrics = this.calculateHalsteadMetrics(ast, sourceCode || ''); const maintainabilityIndex = this.calculateMaintainabilityIndex( cyclomaticComplexity, linesOfCode, halsteadMetrics ); const nestingDepth = this.calculateMaximumNestingDepth(ast); const coupling = this.calculateCoupling(ast); const cohesion = this.calculateCohesion(ast); const metrics: ComplexityMetrics = { cyclomaticComplexity, cognitiveComplexity, halsteadMetrics, linesOfCode, commentLines, maintainabilityIndex, nestingDepth, functionCount, classCount, averageFunctionSize, coupling, cohesion }; const calculationTime = Date.now() - startTime; this.logger.info(`Successfully calculated metrics for: ${filePath}`, { calculationTime, cyclomaticComplexity, cognitiveComplexity, maintainabilityIndex }); return { filePath, metrics, functionMetrics, classMetrics, calculationTime, errors }; } catch (error) { const calculationTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; errors.push(errorMessage); this.logger.error(`Failed to calculate metrics for: ${filePath}`, { error: errorMessage }); return { filePath, metrics: this.getDefaultMetrics(), functionMetrics, classMetrics, calculationTime, errors }; } } private calculateLinesOfCode(sourceCode: string): number { const lines = sourceCode.split('\n'); return lines.filter(line => line.trim() !== '').length; } private calculateCommentLines(sourceCode: string): number { const lines = sourceCode.split('\n'); let commentCount = 0; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('"""') || trimmed.startsWith("'''")) { commentCount++; } } return commentCount; } private findFunctions(ast: ASTNode): ASTNode[] { const functions: ASTNode[] = []; this.traverseAST(ast, (node) => { if (node.type === 'function' || node.type === 'method') { functions.push(node); } }); return functions; } private findClasses(ast: ASTNode): ASTNode[] { const classes: ASTNode[] = []; this.traverseAST(ast, (node) => { if (node.type === 'class') { classes.push(node); } }); return classes; } private calculateFunctionMetrics(func: ASTNode, sourceCode: string): Partial<ComplexityMetrics> { const cyclomaticComplexity = this.calculateCyclomaticComplexity(func); const cognitiveComplexity = this.calculateCognitiveComplexity(func); const nestingDepth = this.calculateMaximumNestingDepth(func); // Calculate function size const funcStartLine = func.start.line - 1; const funcEndLine = func.end.line - 1; const funcLines = sourceCode.split('\n').slice(funcStartLine, funcEndLine + 1); const funcSize = funcLines.filter(line => line.trim() !== '').length; return { cyclomaticComplexity, cognitiveComplexity, nestingDepth, linesOfCode: funcSize }; } private calculateClassMetrics(cls: ASTNode, sourceCode: string): Partial<ComplexityMetrics> { const methods = this.findFunctions(cls); const methodCount = methods.length; // Calculate class size const classStartLine = cls.start.line - 1; const classEndLine = cls.end.line - 1; const classLines = sourceCode.split('\n').slice(classStartLine, classEndLine + 1); const classSize = classLines.filter(line => line.trim() !== '').length; // Calculate average method size const averageMethodSize = methodCount > 0 ? classSize / methodCount : 0; return { functionCount: methodCount, averageFunctionSize: averageMethodSize, linesOfCode: classSize }; } private calculateCyclomaticComplexity(ast: ASTNode): number { let complexity = 1; // Base complexity this.traverseAST(ast, (node) => { // Add complexity for decision points switch (node.type) { case 'IfStatement': case 'ConditionalExpression': case 'SwitchStatement': case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': case 'WhileStatement': case 'DoWhileStatement': case 'CatchClause': complexity += 1; break; case 'LogicalExpression': // Each && or || operator adds complexity if (node.value && (node.value.includes('&&') || node.value.includes('||'))) { const operators = (node.value.match(/&&/g) || []).length + (node.value.match(/\|\|/g) || []).length; complexity += operators; } break; } }); return complexity; } private calculateCognitiveComplexity(ast: ASTNode): number { let complexity = 0; let nestingLevel = 0; this.traverseAST(ast, (node) => { // Reset nesting level for top-level structures if (node.type === 'function' || node.type === 'method' || node.type === 'class') { nestingLevel = 0; } // Add complexity for cognitive load switch (node.type) { case 'IfStatement': case 'ConditionalExpression': complexity += 1 + nestingLevel; nestingLevel += 1; break; case 'SwitchStatement': complexity += 1; nestingLevel += 1; break; case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': case 'WhileStatement': case 'DoWhileStatement': complexity += 1 + nestingLevel; nestingLevel += 1; break; case 'CatchClause': complexity += 1 + nestingLevel; break; case 'LogicalExpression': // Each && or || operator adds cognitive complexity if (node.value && (node.value.includes('&&') || node.value.includes('||'))) { const operators = (node.value.match(/&&/g) || []).length + (node.value.match(/\|\|/g) || []).length; complexity += operators * (1 + nestingLevel); } break; case 'BreakStatement': case 'ContinueStatement': complexity += 1 + nestingLevel; break; } // Reduce nesting level when leaving blocks if (node.body && Array.isArray(node.body)) { // This will be handled by the traversal } }); return complexity; } private calculateHalsteadMetrics(_ast: ASTNode, sourceCode: string): HalsteadMetrics | undefined { try { // Extract operators and operands const operators = this.extractOperators(sourceCode); const operands = this.extractOperands(sourceCode); // Calculate unique operators and operands const uniqueOperators = new Set(operators); const uniqueOperands = new Set(operands); const n1 = uniqueOperators.size; // Unique operators const n2 = uniqueOperands.size; // Unique operands const N1 = operators.length; // Total operators const N2 = operands.length; // Total operands // Calculate Halstead metrics const vocabulary = n1 + n2; const length = N1 + N2; const volume = length * Math.log2(vocabulary); const difficulty = (n1 / 2) * (N2 / n2); const effort = difficulty * volume; const time = effort / 18; // Seconds const bugs = volume / 3000; // Estimated bugs return { difficulty, effort, time, bugs, vocabulary, length, volume }; } catch (error) { this.logger.warn('Failed to calculate Halstead metrics', { error }); return undefined; } } private extractOperators(sourceCode: string): string[] { const operators: string[] = []; // Common operators across languages const operatorPatterns = [ /\+/g, /-/g, /\*/g, /\//g, /%/g, // Arithmetic /==/g, /!=/g, /</g, />/g, /<=/g, />=/g, // Comparison /&&/g, /\|\|/g, /!/g, // Logical /&/g, /\|/g, /\^/g, /~/g, // Bitwise /=/g, /\+=/g, /-=/g, /\*=/g, /\/=/g, /%=/g, // Assignment /<<</g, />>>/g, /<</g, />>/g, // Shift /\+\+/g, /--/g, // Increment/Decrement /\->/g, /\./g, /::/g, // Access ]; for (const pattern of operatorPatterns) { let match; while ((match = pattern.exec(sourceCode)) !== null) { operators.push(match[0]); } } return operators; } private extractOperands(sourceCode: string): string[] { const operands: string[] = []; // Extract numbers const numbers = sourceCode.match(/\b\d+\.?\d*\b/g) || []; operands.push(...numbers); // Extract identifiers (simplified) const identifiers = sourceCode.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) || []; // Filter out keywords const keywords = new Set([ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'return', 'function', 'class', 'interface', 'public', 'private', 'protected', 'static', 'final', 'abstract', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'super', 'import', 'export', 'from', 'as', 'const', 'let', 'var', 'true', 'false', 'null', 'undefined', 'void', 'typeof', 'instanceof', 'in', 'delete', 'with', 'yield', 'await' ]); const filteredIdentifiers = identifiers.filter(id => !keywords.has(id)); operands.push(...filteredIdentifiers); return operands; } private calculateMaintainabilityIndex( cyclomaticComplexity: number, linesOfCode: number, halsteadMetrics?: HalsteadMetrics ): number { // Maintainability Index calculation let volume = 0; if (halsteadMetrics) { volume = halsteadMetrics.volume; } // MI = 171 - 5.2 * ln(avgVol) - 0.23 * avgCyclomatic - 16.2 * ln(avgLOC) // Simplified version for single file const avgVol = volume; const avgCyclomatic = cyclomaticComplexity; const avgLOC = linesOfCode; let mi = 171; if (avgVol > 0) { mi -= 5.2 * Math.log(avgVol); } mi -= 0.23 * avgCyclomatic; if (avgLOC > 0) { mi -= 16.2 * Math.log(avgLOC); } // Normalize to 0-100 scale mi = Math.max(0, Math.min(100, mi)); return mi; } private calculateMaximumNestingDepth(ast: ASTNode): number { let maxDepth = 0; let currentDepth = 0; this.traverseAST(ast, (node) => { // Reset depth for top-level structures if (node.type === 'function' || node.type === 'method' || node.type === 'class') { currentDepth = 0; } // Increase depth for nested structures switch (node.type) { case 'IfStatement': case 'SwitchStatement': case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': case 'WhileStatement': case 'DoWhileStatement': case 'TryStatement': case 'CatchClause': currentDepth += 1; maxDepth = Math.max(maxDepth, currentDepth); break; } }); return maxDepth; } private calculateCoupling(ast: ASTNode): number { let coupling = 0; const dependencies = new Set<string>(); this.traverseAST(ast, (node) => { // Look for external dependencies (simplified) if (node.type === 'statement' && node.value) { // Check for method calls, property access, etc. const methodCalls = node.value.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*\b/g); if (methodCalls) { methodCalls.forEach((call: string) => { const objName = call.split('.')[0]; if (!this.isLocalVariable(objName)) { dependencies.add(objName); } }); } } }); coupling = dependencies.size; return coupling; } private calculateCohesion(ast: ASTNode): number { // Simplified cohesion calculation // LCOM (Lack of Cohesion of Methods) metric const classes = this.findClasses(ast); let totalCohesion = 0; let classCount = 0; for (const cls of classes) { const methods = this.findFunctions(cls); if (methods.length < 2) { continue; } // Calculate method similarity (simplified) let similarPairs = 0; let totalPairs = 0; for (let i = 0; i < methods.length; i++) { for (let j = i + 1; j < methods.length; j++) { totalPairs++; // Simple similarity based on shared variables const similarity = this.calculateMethodSimilarity(methods[i], methods[j]); if (similarity > 0.5) { similarPairs++; } } } const cohesion = totalPairs > 0 ? similarPairs / totalPairs : 0; totalCohesion += cohesion; classCount++; } return classCount > 0 ? totalCohesion / classCount : 0; } private calculateMethodSimilarity(method1: ASTNode, method2: ASTNode): number { // Simplified method similarity calculation const vars1 = this.extractVariables(method1); const vars2 = this.extractVariables(method2); const intersection = new Set([...vars1].filter(x => vars2.has(x))); const union = new Set([...vars1, ...vars2]); return union.size > 0 ? intersection.size / union.size : 0; } private extractVariables(node: ASTNode): Set<string> { const variables = new Set<string>(); this.traverseAST(node, (n) => { if (n.type === 'statement' && n.value) { // Extract variable declarations and usage const varDeclarations = n.value.match(/\b(?:var|let|const)\s+([a-zA-Z_][a-zA-Z0-9_]*)\b/g); if (varDeclarations) { varDeclarations.forEach((decl: string) => { const match = decl.match(/\b(?:var|let|const)\s+([a-zA-Z_][a-zA-Z0-9_]*)\b/); if (match) { variables.add(match[1]); } }); } // Extract variable usage const varUsage = n.value.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g); if (varUsage) { varUsage.forEach((variable: string) => { if (!this.isKeyword(variable)) { variables.add(variable); } }); } } }); return variables; } private isLocalVariable(name: string): boolean { // Simple heuristic to determine if a variable is local const commonLocalVars = new Set([ 'i', 'j', 'k', 'x', 'y', 'z', 'temp', 'result', 'count', 'index', 'sum', 'total', 'avg', 'max', 'min', 'flag', 'found', 'data' ]); return commonLocalVars.has(name.toLowerCase()); } private isKeyword(word: string): boolean { const keywords = new Set([ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'return', 'function', 'class', 'interface', 'public', 'private', 'protected', 'static', 'final', 'abstract', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'super', 'import', 'export', 'from', 'as', 'const', 'let', 'var', 'true', 'false', 'null', 'undefined', 'void', 'typeof', 'instanceof', 'in', 'delete', 'with', 'yield', 'await', 'int', 'string', 'boolean', 'float', 'double', 'char', 'byte' ]); return keywords.has(word); } private traverseAST(ast: ASTNode, visitor: (node: ASTNode) => void): void { visitor(ast); if (ast.body) { for (const child of ast.body) { this.traverseAST(child, visitor); } } if (ast.children) { for (const child of ast.children) { this.traverseAST(child, visitor); } } } private getDefaultMetrics(): ComplexityMetrics { return { cyclomaticComplexity: 1, cognitiveComplexity: 0, linesOfCode: 0, commentLines: 0, maintainabilityIndex: 100, nestingDepth: 0, functionCount: 0, classCount: 0, averageFunctionSize: 0, coupling: 0, cohesion: 0 }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbunting99/mcp-code-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server