Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
MaintainabilityAssessor.ts23 kB
// Copyright 2025 Chris Bunting // Brief: Maintainability Assessor for Code Complexity Analyzer MCP Server // Scope: Assesses code maintainability including technical debt, code churn, and documentation quality import { ASTNode } from './ASTProcessor.js'; import { Logger } from '../utils/Logger.js'; import { Language } from '@mcp-code-analysis/shared-types'; import { ComplexityMetrics } from './MetricCalculator.js'; export interface TechnicalDebt { type: 'code_complexity' | 'code_duplication' | 'lack_of_documentation' | 'outdated_dependencies' | 'test_coverage'; severity: 'low' | 'medium' | 'high' | 'critical'; amount: number; // Estimated effort in person-hours description: string; location: { line: number; column: number }; suggestion: string; priority: number; // 1-10, where 10 is highest priority } export interface CodeChurn { filePath: string; totalChanges: number; recentChanges: number; // Changes in last 30 days hotspots: Array<{ line: number; changeCount: number }>; stability: number; // 0-1, where 1 is most stable lastModified: Date; } export interface DocumentationQuality { coverage: number; // 0-1, percentage of documented elements quality: number; // 0-1, quality of documentation missingDocs: Array<{ element: string; type: string; location: { line: number; column: number } }>; outdatedDocs: Array<{ element: string; issue: string; location: { line: number; column: number } }>; } export interface MaintainabilityAssessment { filePath: string; language: Language; overallScore: number; // 0-100, where 100 is most maintainable technicalDebt: TechnicalDebt[]; codeChurn?: CodeChurn; documentationQuality: DocumentationQuality; maintainabilityFactors: { complexity: number; duplication: number; documentation: number; testability: number; readability: number; }; recommendations: string[]; assessmentTime: number; errors: string[]; } export class MaintainabilityAssessor { private logger: Logger; constructor() { this.logger = new Logger(); } public async assessMaintainability( ast: ASTNode, filePath: string, language: Language, metrics: ComplexityMetrics, sourceCode?: string, gitHistory?: any[] ): Promise<MaintainabilityAssessment> { const startTime = Date.now(); const errors: string[] = []; try { // Assess technical debt const technicalDebt = this.assessTechnicalDebt(ast, metrics, sourceCode || ''); // Analyze code churn (if git history is available) const codeChurn = gitHistory ? this.analyzeCodeChurn(filePath, gitHistory) : undefined; // Assess documentation quality const documentationQuality = this.assessDocumentationQuality(ast, sourceCode || ''); // Calculate maintainability factors const maintainabilityFactors = this.calculateMaintainabilityFactors(metrics, documentationQuality); // Calculate overall maintainability score const overallScore = this.calculateOverallScore(maintainabilityFactors); // Generate recommendations const recommendations = this.generateRecommendations(technicalDebt, maintainabilityFactors); const assessmentTime = Date.now() - startTime; this.logger.info(`Successfully assessed maintainability for: ${filePath}`, { assessmentTime, overallScore, technicalDebtItems: technicalDebt.length, documentationCoverage: documentationQuality.coverage }); return { filePath, language, overallScore, technicalDebt, codeChurn, documentationQuality, maintainabilityFactors, recommendations, assessmentTime, errors }; } catch (error) { const assessmentTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; errors.push(errorMessage); this.logger.error(`Failed to assess maintainability for: ${filePath}`, { error: errorMessage }); return { filePath, language, overallScore: 50, // Default middle score technicalDebt: [], codeChurn: undefined, documentationQuality: { coverage: 0, quality: 0, missingDocs: [], outdatedDocs: [] }, maintainabilityFactors: { complexity: 0.5, duplication: 0.5, documentation: 0.5, testability: 0.5, readability: 0.5 }, recommendations: ['Error in maintainability assessment'], assessmentTime, errors }; } } private assessTechnicalDebt(ast: ASTNode, metrics: ComplexityMetrics, sourceCode: string): TechnicalDebt[] { const technicalDebt: TechnicalDebt[] = []; // Code complexity debt if (metrics.cyclomaticComplexity > 10) { technicalDebt.push({ type: 'code_complexity', severity: metrics.cyclomaticComplexity > 20 ? 'high' : 'medium', amount: Math.max(2, (metrics.cyclomaticComplexity - 10) * 0.5), description: `High cyclomatic complexity (${metrics.cyclomaticComplexity}) indicates complex control flow`, location: { line: 1, column: 0 }, suggestion: 'Refactor complex methods into smaller, more focused functions', priority: Math.min(10, metrics.cyclomaticComplexity) }); } if (metrics.cognitiveComplexity > 15) { technicalDebt.push({ type: 'code_complexity', severity: metrics.cognitiveComplexity > 30 ? 'high' : 'medium', amount: Math.max(2, (metrics.cognitiveComplexity - 15) * 0.3), description: `High cognitive complexity (${metrics.cognitiveComplexity}) makes code hard to understand`, location: { line: 1, column: 0 }, suggestion: 'Simplify nested conditions and reduce cognitive load', priority: Math.min(10, Math.floor(metrics.cognitiveComplexity / 3)) }); } // Code duplication debt const duplications = this.findCodeDuplicates(sourceCode); for (const dup of duplications) { technicalDebt.push({ type: 'code_duplication', severity: dup.lines > 10 ? 'high' : 'medium', amount: dup.lines * 0.2, description: `Code duplication detected (${dup.lines} lines)`, location: { line: dup.startLine, column: 0 }, suggestion: 'Extract duplicated code into reusable functions', priority: Math.min(10, dup.lines) }); } // Lack of documentation debt const undocumentedElements = this.findUndocumentedElements(ast, sourceCode); if (undocumentedElements.length > 3) { technicalDebt.push({ type: 'lack_of_documentation', severity: undocumentedElements.length > 10 ? 'high' : 'medium', amount: undocumentedElements.length * 0.25, description: `${undocumentedElements.length} elements lack proper documentation`, location: { line: 1, column: 0 }, suggestion: 'Add comprehensive documentation for all public APIs and complex logic', priority: Math.min(10, undocumentedElements.length) }); } // Test coverage debt (simplified - would need actual test data) const testCoverage = this.estimateTestCoverage(ast, sourceCode); if (testCoverage < 0.7) { technicalDebt.push({ type: 'test_coverage', severity: testCoverage < 0.3 ? 'high' : 'medium', amount: (1 - testCoverage) * 10, description: `Low test coverage estimated at ${(testCoverage * 100).toFixed(1)}%`, location: { line: 1, column: 0 }, suggestion: 'Increase test coverage to ensure code reliability and maintainability', priority: Math.floor((1 - testCoverage) * 10) }); } // Sort by priority (highest first) technicalDebt.sort((a, b) => b.priority - a.priority); return technicalDebt; } private analyzeCodeChurn(filePath: string, gitHistory: any[]): CodeChurn { const fileHistory = gitHistory.filter(commit => commit.files?.some((f: any) => f.filePath === filePath) ); const totalChanges = fileHistory.reduce((sum, commit) => { const fileCommit = commit.files.find((f: any) => f.filePath === filePath); return sum + (fileCommit?.additions || 0) + (fileCommit?.deletions || 0); }, 0); // Calculate recent changes (last 30 days) const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const recentChanges = fileHistory .filter(commit => new Date(commit.date) > thirtyDaysAgo) .reduce((sum, commit) => { const fileCommit = commit.files.find((f: any) => f.filePath === filePath); return sum + (fileCommit?.additions || 0) + (fileCommit?.deletions || 0); }, 0); // Identify hotspots (frequently changed lines) const hotspots: Array<{ line: number; changeCount: number }> = []; const lineChanges = new Map<number, number>(); fileHistory.forEach(commit => { const fileCommit = commit.files.find((f: any) => f.filePath === filePath); if (fileCommit?.hunks) { fileCommit.hunks.forEach((hunk: any) => { for (let line = hunk.oldStart; line < hunk.oldStart + hunk.oldLines; line++) { lineChanges.set(line, (lineChanges.get(line) || 0) + 1); } }); } }); // Find lines with high change frequency lineChanges.forEach((changeCount, line) => { if (changeCount > 3) { hotspots.push({ line, changeCount }); } }); // Sort hotspots by change count hotspots.sort((a, b) => b.changeCount - a.changeCount); // Calculate stability (inverse of change frequency) const maxPossibleChanges = fileHistory.length * 10; // Arbitrary baseline const stability = Math.max(0, 1 - (totalChanges / maxPossibleChanges)); const lastModified = fileHistory.length > 0 ? new Date(Math.max(...fileHistory.map(c => new Date(c.date).getTime()))) : new Date(); return { filePath, totalChanges, recentChanges, hotspots: hotspots.slice(0, 10), // Top 10 hotspots stability, lastModified }; } private assessDocumentationQuality(ast: ASTNode, sourceCode: string): DocumentationQuality { const totalElements = this.countDocumentableElements(ast); const documentedElements = this.countDocumentedElements(ast, sourceCode); const coverage = totalElements > 0 ? documentedElements / totalElements : 0; const quality = this.assessDocumentationQualityDetailed(ast, sourceCode); const missingDocs = this.findMissingDocumentation(ast, sourceCode); const outdatedDocs = this.findOutdatedDocumentation(ast, sourceCode); return { coverage, quality, missingDocs, outdatedDocs }; } private calculateMaintainabilityFactors( metrics: ComplexityMetrics, documentationQuality: DocumentationQuality ) { // Complexity factor (inverse of complexity metrics) const complexityScore = Math.max(0, 1 - ( (metrics.cyclomaticComplexity / 20) * 0.4 + (metrics.cognitiveComplexity / 30) * 0.3 + (metrics.nestingDepth / 5) * 0.3 )); // Duplication factor (simplified - would need actual duplication analysis) const duplicationScore = 0.8; // Default, would be calculated from actual duplication metrics // Documentation factor const documentationScore = (documentationQuality.coverage + documentationQuality.quality) / 2; // Testability factor (based on complexity and structure) const testabilityScore = Math.max(0, 1 - ( (metrics.cyclomaticComplexity / 15) * 0.5 + (metrics.coupling / 10) * 0.3 + (metrics.cohesion > 0 ? (1 - metrics.cohesion) * 0.2 : 0.2) )); // Readability factor (based on various metrics) const readabilityScore = Math.max(0, 1 - ( (metrics.nestingDepth / 6) * 0.3 + (metrics.averageFunctionSize / 50) * 0.3 + (metrics.linesOfCode > 1000 ? 0.2 : 0) + (metrics.commentLines / metrics.linesOfCode < 0.1 ? 0.2 : 0) )); return { complexity: complexityScore, duplication: duplicationScore, documentation: documentationScore, testability: testabilityScore, readability: readabilityScore }; } private calculateOverallScore(factors: { [key: string]: number }): number { const weights = { complexity: 0.3, duplication: 0.2, documentation: 0.2, testability: 0.15, readability: 0.15 }; let weightedSum = 0; let totalWeight = 0; for (const [factor, score] of Object.entries(factors)) { const weight = weights[factor as keyof typeof weights] || 0.2; weightedSum += score * weight; totalWeight += weight; } return Math.round((weightedSum / totalWeight) * 100); } private generateRecommendations(technicalDebt: TechnicalDebt[], factors: { [key: string]: number }): string[] { const recommendations: string[] = []; // Recommendations based on technical debt for (const debt of technicalDebt.slice(0, 3)) { // Top 3 debts recommendations.push(debt.suggestion); } // Recommendations based on maintainability factors if (factors.complexity < 0.6) { recommendations.push('Focus on reducing code complexity through refactoring and simplification'); } if (factors.documentation < 0.6) { recommendations.push('Improve documentation coverage and quality to enhance code maintainability'); } if (factors.testability < 0.6) { recommendations.push('Improve code testability by reducing dependencies and increasing modularity'); } if (factors.readability < 0.6) { recommendations.push('Enhance code readability through better naming, reduced nesting, and improved structure'); } // General recommendations if (technicalDebt.length > 5) { recommendations.push('Consider implementing a technical debt repayment plan to systematically address issues'); } recommendations.push('Establish code review processes to prevent maintainability issues'); recommendations.push('Set up automated monitoring of maintainability metrics'); return recommendations; } // Helper methods private findCodeDuplicates(sourceCode: string): Array<{ startLine: number; lines: number }> { const duplications: Array<{ startLine: number; lines: number }> = []; const lines = sourceCode.split('\n'); const minBlockSize = 3; for (let i = 0; i < lines.length - minBlockSize; i++) { for (let blockSize = minBlockSize; blockSize <= Math.min(10, lines.length - i); blockSize++) { const block = lines.slice(i, i + blockSize).join('\n'); // Look for the same block elsewhere for (let j = i + blockSize; j < lines.length - blockSize + 1; j++) { const otherBlock = lines.slice(j, j + blockSize).join('\n'); if (block === otherBlock && block.trim().length > 0) { duplications.push({ startLine: i + 1, lines: blockSize }); break; // Only count each duplication once } } } } return duplications; } private findUndocumentedElements(ast: ASTNode, sourceCode: string): Array<{ element: string; type: string; line: number }> { const undocumented: Array<{ element: string; type: string; line: number }> = []; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method' || node.type === 'class') && node.name) { const hasDocumentation = this.hasDocumentation(node, sourceCode); if (!hasDocumentation) { undocumented.push({ element: node.name, type: node.type, line: node.start.line }); } } }); return undocumented; } private hasDocumentation(node: ASTNode, sourceCode: string): boolean { const lines = sourceCode.split('\n'); const nodeLine = node.start.line - 1; // Check for documentation comments before the node if (nodeLine > 0) { const prevLine = lines[nodeLine - 1].trim(); return prevLine.startsWith('/**') || prevLine.startsWith('/*') || prevLine.startsWith('///') || prevLine.startsWith('#') || prevLine.startsWith('--'); } return false; } private countDocumentableElements(ast: ASTNode): number { let count = 0; this.traverseAST(ast, (node) => { if (node.type === 'function' || node.type === 'method' || node.type === 'class') { count++; } }); return count; } private countDocumentedElements(ast: ASTNode, sourceCode: string): number { let count = 0; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method' || node.type === 'class') && node.name) { if (this.hasDocumentation(node, sourceCode)) { count++; } } }); return count; } private assessDocumentationQualityDetailed(ast: ASTNode, sourceCode: string): number { let totalScore = 0; let elementCount = 0; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method' || node.type === 'class') && node.name) { const score = this.assessElementDocumentationQuality(node, sourceCode); totalScore += score; elementCount++; } }); return elementCount > 0 ? totalScore / elementCount : 0; } private assessElementDocumentationQuality(node: ASTNode, sourceCode: string): number { if (!this.hasDocumentation(node, sourceCode)) { return 0; } const lines = sourceCode.split('\n'); const nodeLine = node.start.line - 1; const docLines: string[] = []; // Extract documentation for (let i = nodeLine - 1; i >= 0; i--) { const line = lines[i].trim(); if (line.startsWith('/**') || line.startsWith('/*') || line.startsWith('///') || line.startsWith('#') || line.startsWith('--')) { docLines.unshift(line); } else if (line.length > 0 && !line.startsWith('*') && !line.startsWith('//')) { break; } } const docText = docLines.join('\n'); // Quality assessment criteria let score = 0; // Has description if (docText.length > 20) { score += 0.3; } // Has parameter documentation if (docText.includes('@param') || docText.includes('Parameter') || docText.includes('Args:')) { score += 0.3; } // Has return value documentation if (docText.includes('@return') || docText.includes('Returns') || docText.includes('=>')) { score += 0.2; } // Has examples if (docText.includes('@example') || docText.includes('Example') || docText.includes('e.g.')) { score += 0.1; } // Has version/author information if (docText.includes('@version') || docText.includes('@author') || docText.includes('Since:')) { score += 0.1; } return Math.min(1, score); } private findMissingDocumentation(ast: ASTNode, sourceCode: string): Array<{ element: string; type: string; location: { line: number; column: number } }> { const missing: Array<{ element: string; type: string; location: { line: number; column: number } }> = []; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method' || node.type === 'class') && node.name) { if (!this.hasDocumentation(node, sourceCode)) { missing.push({ element: node.name, type: node.type, location: { line: node.start.line, column: node.start.column } }); } } }); return missing; } private findOutdatedDocumentation(ast: ASTNode, sourceCode: string): Array<{ element: string; issue: string; location: { line: number; column: number } }> { // Simplified implementation - in practice, this would require more sophisticated analysis const outdated: Array<{ element: string; issue: string; location: { line: number; column: number } }> = []; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method') && node.name) { if (this.hasDocumentation(node, sourceCode)) { // Check for common signs of outdated documentation const lines = sourceCode.split('\n'); const nodeLine = node.start.line - 1; const docLines: string[] = []; for (let i = nodeLine - 1; i >= 0; i--) { const line = lines[i].trim(); if (line.startsWith('/**') || line.startsWith('/*') || line.startsWith('///') || line.startsWith('#') || line.startsWith('--')) { docLines.unshift(line); } else if (line.length > 0 && !line.startsWith('*') && !line.startsWith('//')) { break; } } const docText = docLines.join('\n'); // Look for TODO markers or outdated version references if (docText.includes('TODO') || docText.includes('FIXME')) { outdated.push({ element: node.name, issue: 'Documentation contains TODO/FIXME markers', location: { line: node.start.line, column: node.start.column } }); } if (docText.match(/\b(19|20)\d{2}\b/)) { outdated.push({ element: node.name, issue: 'Documentation contains outdated year references', location: { line: node.start.line, column: node.start.column } }); } } } }); return outdated; } private estimateTestCoverage(ast: ASTNode, sourceCode: string): number { // Simplified test coverage estimation // In practice, this would use actual test coverage data let testableElements = 0; let testIndicators = 0; this.traverseAST(ast, (node) => { if ((node.type === 'function' || node.type === 'method') && node.name) { testableElements++; // Look for test indicators in the code if (node.name.toLowerCase().includes('test') || node.name.toLowerCase().includes('spec') || (sourceCode.toLowerCase().includes('assert') && sourceCode.toLowerCase().includes(node.name.toLowerCase()))) { testIndicators++; } } }); return testableElements > 0 ? testIndicators / testableElements : 0.5; // Default 50% } 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); } } } }

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