Skip to main content
Glama
code-analyzer.ts9.49 kB
import type { CodeFile, CodeQualityMetrics, CodeSmell, CodeAnalysisOptions } from '../types/index.js'; import { FileReader } from '../utils/file-reader.js'; export class CodeAnalyzer { /** * Analyze code quality for a file or set of files */ async analyzeCodeQuality( files: CodeFile[] | string[], options?: CodeAnalysisOptions ): Promise<CodeQualityMetrics> { const codeFiles = await this.getCodeFiles(files); const totalLines = codeFiles.reduce((sum, file) => sum + file.lines, 0); const complexity = this.calculateAverageComplexity(codeFiles); const maintainabilityIndex = this.calculateMaintainabilityIndex(codeFiles, complexity); const codeSmells = options?.checkCodeSmells !== false ? this.detectCodeSmells(codeFiles, options) : []; const duplications = options?.checkDuplicates !== false ? await this.findDuplications(codeFiles, options) : []; return { complexity, maintainabilityIndex, technicalDebt: this.estimateTechnicalDebt(codeSmells, duplications.length), codeSmells, duplications, linesOfCode: totalLines, cyclomaticComplexity: complexity, }; } /** * Calculate cyclomatic complexity for code */ calculateComplexity(code: string): number { // Simple complexity calculation based on control flow statements const complexityKeywords = [ /\bif\s*\(/g, /\belse\s*{/g, /\bfor\s*\(/g, /\bwhile\s*\(/g, /\bswitch\s*\(/g, /\bcase\s+/g, /\bcatch\s*\(/g, /\b&&/g, /\b\|\|/g, /\?\s*.*\s*:/g, // ternary operator ]; let complexity = 1; // Base complexity for (const pattern of complexityKeywords) { const matches = code.match(pattern); if (matches) { complexity += matches.length; } } return complexity; } /** * Calculate average complexity for multiple files */ private calculateAverageComplexity(files: CodeFile[]): number { if (files.length === 0) return 0; const totalComplexity = files.reduce((sum, file) => { return sum + this.calculateComplexity(file.content); }, 0); return Math.round((totalComplexity / files.length) * 100) / 100; } /** * Calculate maintainability index * Formula: MI = 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code) */ private calculateMaintainabilityIndex(files: CodeFile[], complexity: number): number { if (files.length === 0) return 100; const totalLines = files.reduce((sum, file) => sum + file.lines, 0); const avgLines = totalLines / files.length; // Simplified maintainability index calculation const halsteadVolume = this.estimateHalsteadVolume(files); const mi = 171 - 5.2 * Math.log(halsteadVolume || 1) - 0.23 * complexity - 16.2 * Math.log(avgLines || 1); // Clamp between 0 and 100 return Math.max(0, Math.min(100, Math.round(mi * 100) / 100)); } /** * Estimate Halstead Volume (simplified) */ private estimateHalsteadVolume(files: CodeFile[]): number { // Simplified estimation based on unique operators and operands const operators = new Set<string>(); const operands = new Set<string>(); for (const file of files) { // Extract operators (simplified) const operatorPattern = /[+\-*/%=<>!&|?:,;.(){}[\]]/g; const operatorMatches = file.content.match(operatorPattern); if (operatorMatches) { operatorMatches.forEach((op) => operators.add(op)); } // Extract operands (identifiers) const identifierPattern = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g; const identifierMatches = file.content.match(identifierPattern); if (identifierMatches) { identifierMatches.forEach((id) => operands.add(id)); } } const n1 = operators.size; // Unique operators const n2 = operands.size; // Unique operands const N = files.reduce((sum, file) => sum + file.content.length, 0); // Total length return N * Math.log2(n1 + n2 || 1); } /** * Detect code smells in code files */ private detectCodeSmells(files: CodeFile[], options?: CodeAnalysisOptions): CodeSmell[] { const smells: CodeSmell[] = []; for (const file of files) { const lines = file.content.split('\n'); // Long method detection if (file.lines > 100) { smells.push({ type: 'long_method', severity: file.lines > 200 ? 'high' : 'medium', location: file.path, description: `Method/file is too long (${file.lines} lines). Consider breaking it down.`, suggestion: 'Extract methods or split into multiple files.', }); } // High complexity detection const complexity = this.calculateComplexity(file.content); if (complexity > (options?.maxComplexity || 10)) { smells.push({ type: 'high_complexity', severity: complexity > 20 ? 'high' : 'medium', location: file.path, line: 1, description: `High cyclomatic complexity (${complexity}).`, suggestion: 'Refactor to reduce complexity by extracting methods.', }); } // Deep nesting detection let maxNesting = 0; let currentNesting = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const openBraces = (line.match(/{/g) || []).length; const closeBraces = (line.match(/}/g) || []).length; currentNesting += openBraces - closeBraces; maxNesting = Math.max(maxNesting, currentNesting); } if (maxNesting > 4) { smells.push({ type: 'deep_nesting', severity: maxNesting > 6 ? 'high' : 'medium', location: file.path, description: `Deep nesting detected (${maxNesting} levels).`, suggestion: 'Extract nested code into separate functions.', }); } // Magic numbers detection const magicNumberPattern = /\b\d{3,}\b/g; const magicNumbers = file.content.match(magicNumberPattern); if (magicNumbers && magicNumbers.length > 5) { smells.push({ type: 'magic_numbers', severity: 'low', location: file.path, description: `Multiple magic numbers detected (${magicNumbers.length}).`, suggestion: 'Extract magic numbers into named constants.', }); } // Duplicate code detection (simple) const duplicatePattern = /(.{20,})\1{2,}/g; if (duplicatePattern.test(file.content)) { smells.push({ type: 'duplicate_code', severity: 'medium', location: file.path, description: 'Potential duplicate code detected.', suggestion: 'Extract common code into reusable functions.', }); } } return smells; } /** * Find code duplications (simplified - would use jscpd in production) */ private async findDuplications( files: CodeFile[], options?: CodeAnalysisOptions ): Promise<import('../types/index.js').Duplication[]> { // This is a simplified version. In production, we'd use jscpd library const duplications: import('../types/index.js').Duplication[] = []; const minLines = options?.minLinesForDuplication || 5; for (let i = 0; i < files.length; i++) { for (let j = i + 1; j < files.length; j++) { const similarity = this.calculateSimilarity(files[i].content, files[j].content); if (similarity > 0.7 && files[i].lines >= minLines) { duplications.push({ lines: files[i].lines, tokens: 0, firstFile: files[i].path, secondFile: files[j].path, startLine1: 1, endLine1: files[i].lines, startLine2: 1, endLine2: files[j].lines, }); } } } return duplications; } /** * Calculate similarity between two code strings (simplified) */ private calculateSimilarity(code1: string, code2: string): number { // Simple similarity based on common lines const lines1 = code1.split('\n').filter((l) => l.trim().length > 0); const lines2 = code2.split('\n').filter((l) => l.trim().length > 0); if (lines1.length === 0 || lines2.length === 0) return 0; const commonLines = lines1.filter((line) => lines2.includes(line)).length; const maxLines = Math.max(lines1.length, lines2.length); return commonLines / maxLines; } /** * Estimate technical debt */ private estimateTechnicalDebt(codeSmells: CodeSmell[], duplicationCount: number): string { const totalIssues = codeSmells.length + duplicationCount; const criticalIssues = codeSmells.filter((s) => s.severity === 'critical').length; const highIssues = codeSmells.filter((s) => s.severity === 'high').length; if (totalIssues === 0) return 'None'; if (criticalIssues > 0) return 'Critical'; if (highIssues > 5) return 'High'; if (totalIssues > 20) return 'Medium'; return 'Low'; } /** * Get code files from paths or CodeFile objects */ private async getCodeFiles(files: CodeFile[] | string[]): Promise<CodeFile[]> { if (files.length === 0) return []; if (typeof files[0] === 'string') { return await FileReader.readFiles((files as string[]).join(','), { ignore: ['node_modules/**', 'dist/**', 'build/**'], }); } return files as CodeFile[]; } }

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/code-alchemist01/development-tools-mcp-Server'

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