UNPKG

@zubenelakrab/gitstats

Version:

Powerful Git repository analyzer with comprehensive statistics and insights

352 lines 15.9 kB
export class ComplexityAnalyzer { name = 'complexity-analyzer'; description = 'Analyzes code complexity and identifies problematic files'; async analyze(commits, _config) { const fileStats = new Map(); // Aggregate file statistics for (const commit of commits) { for (const file of commit.files) { if (!fileStats.has(file.path)) { fileStats.set(file.path, { additions: 0, deletions: 0, commits: 0, authors: new Set(), }); } const stats = fileStats.get(file.path); stats.additions += file.additions; stats.deletions += file.deletions; stats.commits++; stats.authors.add(commit.author.email); } } // Identify god files (high total changes + many commits + many authors) const godFiles = []; const godThreshold = 5000; // total changes threshold for (const [path, stats] of fileStats) { const totalChanges = stats.additions + stats.deletions; if (totalChanges > godThreshold || (stats.commits > 50 && stats.authors.size > 5)) { const reasons = []; if (totalChanges > godThreshold) reasons.push(`${totalChanges} total line changes`); if (stats.commits > 50) reasons.push(`${stats.commits} commits`); if (stats.authors.size > 5) reasons.push(`${stats.authors.size} authors`); godFiles.push({ path, totalChanges, commitCount: stats.commits, authorCount: stats.authors.size, reason: reasons.join(', '), }); } } godFiles.sort((a, b) => b.totalChanges - a.totalChanges); // Identify growing files const growingFiles = []; for (const [path, stats] of fileStats) { if (stats.commits < 3) continue; // Need enough data const netGrowth = stats.additions - stats.deletions; const growthRate = netGrowth / stats.commits; let trend; if (growthRate > 10) { trend = 'growing'; } else if (growthRate < -10) { trend = 'shrinking'; } else { trend = 'stable'; } if (trend !== 'stable') { growingFiles.push({ path, netGrowth, growthRate, commitCount: stats.commits, trend, }); } } growingFiles.sort((a, b) => Math.abs(b.growthRate) - Math.abs(a.growthRate)); // Identify refactoring candidates const refactoringCandidates = []; for (const [path, stats] of fileStats) { if (stats.commits < 5 || stats.additions < 100) continue; const ratio = stats.deletions > 0 ? stats.additions / stats.deletions : stats.additions; let suggestion = ''; if (ratio > 5) { suggestion = 'Code accumulating - consider refactoring'; } else if (ratio > 3) { suggestion = 'Growing faster than being cleaned up'; } else if (ratio < 0.5 && stats.deletions > 100) { suggestion = 'Good refactoring activity'; } if (suggestion && ratio > 2) { refactoringCandidates.push({ path, addDeleteRatio: ratio, totalAdditions: stats.additions, totalDeletions: stats.deletions, commitCount: stats.commits, suggestion, }); } } refactoringCandidates.sort((a, b) => b.addDeleteRatio - a.addDeleteRatio); // Calculate overall metrics let totalGrowth = 0; let filesWithHighChurn = 0; for (const [, stats] of fileStats) { totalGrowth += stats.additions - stats.deletions; if (stats.commits > 20) filesWithHighChurn++; } // Calculate Technical Debt Analysis const { technicalDebtScore, debtByModule, criticalDebtAreas, debtIndicators } = this.calculateTechnicalDebt(fileStats, godFiles, growingFiles, refactoringCandidates); // Determine debt trend const growingCount = growingFiles.filter(f => f.trend === 'growing').length; const shrinkingCount = growingFiles.filter(f => f.trend === 'shrinking').length; let debtTrend = 'stable'; if (growingCount > shrinkingCount * 2) debtTrend = 'increasing'; else if (shrinkingCount > growingCount * 2) debtTrend = 'decreasing'; // Calculate critical hotspots (files with high churn AND high changes) const criticalHotspots = this.calculateCriticalHotspots(fileStats); return { godFiles: godFiles.slice(0, 20), growingFiles: growingFiles.slice(0, 20), refactoringCandidates: refactoringCandidates.slice(0, 20), averageFileGrowth: fileStats.size > 0 ? totalGrowth / fileStats.size : 0, totalFilesAnalyzed: fileStats.size, filesWithHighChurn, technicalDebtScore, debtByModule, debtTrend, criticalDebtAreas, debtIndicators, criticalHotspots, }; } calculateTechnicalDebt(fileStats, godFiles, growingFiles, refactoringCandidates) { // Group files by top-level module/directory const moduleStats = new Map(); const getModule = (path) => { const parts = path.split('/'); return parts.length > 1 ? parts.slice(0, 2).join('/') : parts[0]; }; // Initialize modules for (const [path, stats] of fileStats) { const module = getModule(path); if (!moduleStats.has(module)) { moduleStats.set(module, { files: 0, filesWithDebt: 0, totalChurn: 0, totalGrowth: 0, issues: [] }); } const mod = moduleStats.get(module); mod.files++; mod.totalChurn += stats.commits; mod.totalGrowth += stats.additions - stats.deletions; // Check if file has debt indicators const hasDebt = stats.commits > 20 || (stats.additions / Math.max(stats.deletions, 1)) > 5 || stats.authors.size === 1 && stats.commits > 10; if (hasDebt) mod.filesWithDebt++; } // Add issues from god files for (const gf of godFiles) { const module = getModule(gf.path); const mod = moduleStats.get(module); if (mod && !mod.issues.includes('God files detected')) { mod.issues.push('God files detected'); } } // Add issues from growing files for (const gf of growingFiles) { const module = getModule(gf.path); const mod = moduleStats.get(module); if (mod && gf.trend === 'growing' && !mod.issues.includes('Files growing rapidly')) { mod.issues.push('Files growing rapidly'); } } // Calculate module debt scores const debtByModule = []; for (const [path, stats] of moduleStats) { if (stats.files < 3) continue; const debtRatio = stats.filesWithDebt / stats.files; const debtScore = Math.round(debtRatio * 100); debtByModule.push({ path, debtScore, filesWithDebt: stats.filesWithDebt, totalFiles: stats.files, topIssues: stats.issues.slice(0, 3), }); } debtByModule.sort((a, b) => b.debtScore - a.debtScore); // Calculate critical debt areas const criticalDebtAreas = []; for (const gf of godFiles.slice(0, 10)) { const stats = fileStats.get(gf.path); if (!stats) continue; const churnRate = stats.commits; const growthRate = stats.additions - stats.deletions; const authorConcentration = 1 / stats.authors.size; criticalDebtAreas.push({ path: gf.path, debtScore: Math.min(100, Math.round(gf.totalChanges / 100)), reason: gf.reason, metrics: { churnRate, growthRate, authorConcentration }, recommendation: this.getDebtRecommendation(churnRate, growthRate, authorConcentration), }); } // Calculate debt indicators const totalFiles = fileStats.size; const godFilePercentage = (godFiles.length / totalFiles) * 100; const growingFilesPercentage = (growingFiles.filter(f => f.trend === 'growing').length / totalFiles) * 100; const refactorNeededPercentage = (refactoringCandidates.length / totalFiles) * 100; const debtIndicators = [ { name: 'God Files', value: godFiles.length, status: godFiles.length < 5 ? 'good' : godFiles.length < 15 ? 'warning' : 'critical', description: `${godFilePercentage.toFixed(1)}% of files are "god files"`, }, { name: 'Growing Files', value: growingFiles.filter(f => f.trend === 'growing').length, status: growingFilesPercentage < 5 ? 'good' : growingFilesPercentage < 15 ? 'warning' : 'critical', description: `${growingFilesPercentage.toFixed(1)}% of files are growing rapidly`, }, { name: 'Refactor Needed', value: refactoringCandidates.length, status: refactorNeededPercentage < 10 ? 'good' : refactorNeededPercentage < 25 ? 'warning' : 'critical', description: `${refactorNeededPercentage.toFixed(1)}% of files need refactoring`, }, ]; // Calculate high churn files count const highChurnCount = Array.from(fileStats.values()).filter(s => s.commits > 20).length; debtIndicators.push({ name: 'High Churn Files', value: highChurnCount, status: highChurnCount < 20 ? 'good' : highChurnCount < 50 ? 'warning' : 'critical', description: 'Files with more than 20 commits', }); // Calculate overall technical debt score let technicalDebtScore = 0; for (const indicator of debtIndicators) { if (indicator.status === 'critical') technicalDebtScore += 25; else if (indicator.status === 'warning') technicalDebtScore += 10; } technicalDebtScore = Math.min(100, technicalDebtScore); return { technicalDebtScore, debtByModule: debtByModule.slice(0, 15), criticalDebtAreas, debtIndicators }; } getDebtRecommendation(churnRate, growthRate, authorConcentration) { if (authorConcentration > 0.8) { return 'Single owner - spread knowledge through pair programming or code reviews'; } if (growthRate > 500) { return 'File growing too fast - consider splitting into smaller modules'; } if (churnRate > 50) { return 'High change frequency - stabilize API or add better abstractions'; } return 'Monitor and refactor incrementally'; } calculateCriticalHotspots(fileStats) { const hotspots = []; // Calculate thresholds based on distribution const allCommits = Array.from(fileStats.values()).map(s => s.commits); const allChanges = Array.from(fileStats.values()).map(s => s.additions + s.deletions); const avgCommits = allCommits.reduce((a, b) => a + b, 0) / (allCommits.length || 1); const avgChanges = allChanges.reduce((a, b) => a + b, 0) / (allChanges.length || 1); // Files are critical if they have BOTH high churn AND high changes const highChurnThreshold = Math.max(avgCommits * 2, 10); const highChangesThreshold = Math.max(avgChanges * 2, 500); for (const [path, stats] of fileStats) { const totalChanges = stats.additions + stats.deletions; // Both conditions must be true for a critical hotspot if (stats.commits >= highChurnThreshold && totalChanges >= highChangesThreshold) { const changeVelocity = totalChanges / stats.commits; const riskFactors = []; // Calculate risk score components let riskScore = 0; // High churn factor (max 30 points) const churnFactor = Math.min(30, (stats.commits / highChurnThreshold) * 15); riskScore += churnFactor; if (stats.commits > highChurnThreshold * 2) { riskFactors.push(`Very high churn (${stats.commits} commits)`); } else { riskFactors.push(`High churn (${stats.commits} commits)`); } // High changes factor (max 30 points) const changesFactor = Math.min(30, (totalChanges / highChangesThreshold) * 15); riskScore += changesFactor; if (totalChanges > highChangesThreshold * 2) { riskFactors.push(`Very high changes (${totalChanges} LOC)`); } else { riskFactors.push(`High changes (${totalChanges} LOC)`); } // Author concentration factor (max 20 points) if (stats.authors.size === 1) { riskScore += 20; riskFactors.push('Single author (bus factor risk)'); } else if (stats.authors.size <= 2) { riskScore += 10; riskFactors.push('Few authors (knowledge concentration)'); } // High velocity factor (max 20 points) if (changeVelocity > 100) { riskScore += 20; riskFactors.push('High change velocity per commit'); } else if (changeVelocity > 50) { riskScore += 10; riskFactors.push('Moderate change velocity'); } riskScore = Math.min(100, Math.round(riskScore)); let riskLevel; if (riskScore >= 70) { riskLevel = 'critical'; } else if (riskScore >= 50) { riskLevel = 'high'; } else { riskLevel = 'medium'; } hotspots.push({ path, commitCount: stats.commits, totalChanges, authorCount: stats.authors.size, changeVelocity: Math.round(changeVelocity), riskScore, riskLevel, riskFactors, }); } } // Sort by risk score descending hotspots.sort((a, b) => b.riskScore - a.riskScore); return hotspots.slice(0, 15); } } export function createComplexityAnalyzer() { return new ComplexityAnalyzer(); } //# sourceMappingURL=complexity-analyzer.js.map