UNPKG

aetherlight-analyzer

Version:

Code analysis tool to generate ÆtherLight sprint plans from any codebase

543 lines 25.5 kB
"use strict"; /** * DESIGN DECISION: Multi-category technical debt detection with severity scoring * WHY: Technical debt accumulates silently - need automated detection and prioritization * * REASONING CHAIN: * 1. Scan source files for debt indicators (TODO, FIXME, HACK comments) * 2. Detect code smells (magic numbers, hardcoded strings, long methods) * 3. Identify missing error handling (no try/catch, no error checking) * 4. Find deprecated API usage (via pattern matching) * 5. Calculate debt score (0-100, weighted by severity and frequency) * 6. Generate prioritized issue list for sprint planning * 7. Result: Clear roadmap for technical debt reduction * * PATTERN: Pattern-ANALYZER-001 (AST-Based Code Analysis) * RELATED: TypeScript Parser, Rust Parser * PERFORMANCE: <2s for 100k LOC */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TechnicalDebtAnalyzer = void 0; const fs = __importStar(require("fs")); const types_1 = require("../parsers/types"); const types_2 = require("./types"); class TechnicalDebtAnalyzer { /** * Analyze technical debt in parsed codebase * * DESIGN DECISION: Combine static analysis (AST) with text analysis (comments, strings) * WHY: Some debt visible in AST (long methods), some only in source text (TODO comments) */ analyze(parseResult) { const startTime = Date.now(); const issues = []; // Phase 1: Scan for comment-based debt (TODO, FIXME, HACK) issues.push(...this.detectCommentDebt(parseResult)); // Phase 2: Detect code smells from AST issues.push(...this.detectCodeSmells(parseResult)); // Phase 3: Detect magic numbers and hardcoded strings issues.push(...this.detectHardcodedValues(parseResult)); // Phase 4: Detect missing error handling issues.push(...this.detectMissingErrorHandling(parseResult)); // Phase 5: Detect deprecated API usage issues.push(...this.detectDeprecatedAPIs(parseResult)); // Calculate debt score const score = this.calculateDebtScore(issues); // Categorize issues const categories = this.categorizeIssues(issues); // Count by severity const highPriority = issues.filter((i) => i.severity === types_2.IssueSeverity.HIGH).length; const mediumPriority = issues.filter((i) => i.severity === types_2.IssueSeverity.MEDIUM).length; const lowPriority = issues.filter((i) => i.severity === types_2.IssueSeverity.LOW).length; const analysis = { totalIssues: issues.length, highPriority, mediumPriority, lowPriority, categories, issues, score, }; return { name: 'technical-debt', version: '1.0.0', executionTimeMs: Date.now() - startTime, data: analysis, }; } /** * Detect TODO/FIXME/HACK comments in source files * * REASONING CHAIN: * 1. Read source file contents (not available in AST) * 2. Scan for comment patterns (// TODO, /* FIXME *\/, # HACK) * 3. Extract line number and surrounding context * 4. Classify severity (FIXME > TODO > HACK) */ detectCommentDebt(parseResult) { const issues = []; parseResult.files.forEach((file) => { try { const content = fs.readFileSync(file.filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { const trimmed = line.trim(); // TODO detection if (trimmed.match(/\/\/\s*TODO|\/\*\s*TODO|\#\s*TODO/i)) { issues.push({ category: types_2.TechnicalDebtCategory.TODO, severity: types_2.IssueSeverity.LOW, location: { filePath: file.filePath, line: index + 1, context: trimmed, }, description: `TODO comment: ${trimmed.substring(0, 100)}`, recommendation: 'Create task to address this TODO or remove if no longer relevant.', }); } // FIXME detection if (trimmed.match(/\/\/\s*FIXME|\/\*\s*FIXME|\#\s*FIXME/i)) { issues.push({ category: types_2.TechnicalDebtCategory.FIXME, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: index + 1, context: trimmed, }, description: `FIXME comment: ${trimmed.substring(0, 100)}`, recommendation: 'Prioritize fixing this issue - FIXME indicates a known problem.', }); } // HACK detection if (trimmed.match(/\/\/\s*HACK|\/\*\s*HACK|\#\s*HACK/i)) { issues.push({ category: types_2.TechnicalDebtCategory.HACK, severity: types_2.IssueSeverity.HIGH, location: { filePath: file.filePath, line: index + 1, context: trimmed, }, description: `HACK comment: ${trimmed.substring(0, 100)}`, recommendation: 'Refactor this workaround into a proper solution - HACKs indicate fragile code.', }); } }); } catch (error) { // File read error - skip this file } }); return issues; } /** * Detect code smells from AST * * DESIGN DECISION: Focus on objectively measurable smells * WHY: Subjective smells (naming, style) are less actionable */ detectCodeSmells(parseResult) { const issues = []; parseResult.files.forEach((file) => { file.elements.forEach((elem) => { // Long method detection (>50 lines) if (elem.type === types_1.ElementType.FUNCTION || elem.type === types_1.ElementType.METHOD) { const funcElem = elem; if (elem.location.endLine && elem.location.line) { const lineCount = elem.location.endLine - elem.location.line; if (lineCount > 50) { issues.push({ category: types_2.TechnicalDebtCategory.LONG_METHOD, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: elem.location.line, }, description: `Function '${elem.name}' is ${lineCount} lines long (threshold: 50)`, recommendation: 'Extract code blocks into smaller, focused functions.', }); } } } // God class detection (>20 methods or >500 lines) if (elem.type === types_1.ElementType.CLASS) { const classElem = elem; const methodCount = classElem.methods?.length || 0; if (methodCount > 20) { issues.push({ category: types_2.TechnicalDebtCategory.GOD_CLASS, severity: types_2.IssueSeverity.HIGH, location: { filePath: file.filePath, line: elem.location.line, }, description: `Class '${elem.name}' has ${methodCount} methods (threshold: 20)`, recommendation: 'Split this class into multiple smaller classes with focused responsibilities.', }); } // Check line count if (elem.location.endLine && elem.location.line) { const lineCount = elem.location.endLine - elem.location.line; if (lineCount > 500) { issues.push({ category: types_2.TechnicalDebtCategory.GOD_CLASS, severity: types_2.IssueSeverity.HIGH, location: { filePath: file.filePath, line: elem.location.line, }, description: `Class '${elem.name}' is ${lineCount} lines long (threshold: 500)`, recommendation: 'Refactor into multiple smaller classes.', }); } } } }); }); return issues; } /** * Detect hardcoded values (magic numbers, hardcoded strings) * * REASONING CHAIN: * 1. Read source file contents * 2. Use regex to find numeric literals (not 0, 1, -1, 100) * 3. Find hardcoded URLs, paths, API keys patterns * 4. Recommend extraction to constants or config */ detectHardcodedValues(parseResult) { const issues = []; parseResult.files.forEach((file) => { try { const content = fs.readFileSync(file.filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { // Skip comments if (line.trim().startsWith('//') || line.trim().startsWith('/*')) { return; } // Magic number detection (exclude common values: 0, 1, -1, 2, 10, 100, 1000) const magicNumberPattern = /(?<![\w])\b(?!0\b|1\b|-1\b|2\b|10\b|100\b|1000\b)\d{2,}\b/g; const magicNumbers = line.match(magicNumberPattern); if (magicNumbers) { issues.push({ category: types_2.TechnicalDebtCategory.MAGIC_NUMBER, severity: types_2.IssueSeverity.LOW, location: { filePath: file.filePath, line: index + 1, context: line.trim().substring(0, 100), }, description: `Magic number detected: ${magicNumbers.join(', ')}`, recommendation: 'Extract magic numbers to named constants with descriptive names.', }); } // Hardcoded URL detection if (line.match(/https?:\/\/[^\s\)'"]+/)) { issues.push({ category: types_2.TechnicalDebtCategory.HARDCODED_STRING, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: index + 1, context: line.trim().substring(0, 100), }, description: 'Hardcoded URL detected', recommendation: 'Move URL to configuration file or environment variable.', }); } // Hardcoded file path detection if (line.match(/['"]\/[^\s'"]+['"]/)) { issues.push({ category: types_2.TechnicalDebtCategory.HARDCODED_STRING, severity: types_2.IssueSeverity.LOW, location: { filePath: file.filePath, line: index + 1, context: line.trim().substring(0, 100), }, description: 'Hardcoded file path detected', recommendation: 'Move file paths to configuration or use path.join().', }); } // Potential API key pattern (long alphanumeric strings) const apiKeyPattern = /['"][A-Za-z0-9]{32,}['"]/; if (line.match(apiKeyPattern)) { issues.push({ category: types_2.TechnicalDebtCategory.HARDCODED_STRING, severity: types_2.IssueSeverity.HIGH, location: { filePath: file.filePath, line: index + 1, context: '[REDACTED]', }, description: 'Potential API key or secret detected', recommendation: 'CRITICAL: Move secrets to environment variables or secret management system.', }); } }); } catch (error) { // File read error - skip } }); return issues; } /** * Detect missing error handling * * DESIGN DECISION: Heuristic-based detection (not perfect, but useful) * WHY: Full error flow analysis requires complex dataflow analysis */ detectMissingErrorHandling(parseResult) { const issues = []; parseResult.files.forEach((file) => { try { const content = fs.readFileSync(file.filePath, 'utf-8'); const lines = content.split('\n'); // Track if we're in a try block let inTryBlock = false; let tryBlockDepth = 0; lines.forEach((line, index) => { const trimmed = line.trim(); // Track try/catch blocks if (trimmed.includes('try {')) { inTryBlock = true; tryBlockDepth++; } if (trimmed.includes('} catch')) { tryBlockDepth--; if (tryBlockDepth === 0) { inTryBlock = false; } } // Detect async functions without error handling if ((trimmed.includes('await ') || trimmed.includes('.then(')) && !inTryBlock && !trimmed.includes('.catch(')) { issues.push({ category: types_2.TechnicalDebtCategory.MISSING_ERROR_HANDLING, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: index + 1, context: trimmed.substring(0, 100), }, description: 'Async operation without error handling', recommendation: 'Wrap in try/catch or add .catch() handler.', }); } // Detect file operations without error handling if ((trimmed.includes('fs.readFile') || trimmed.includes('fs.writeFile') || trimmed.includes('fs.') || trimmed.includes('readFileSync') || trimmed.includes('writeFileSync')) && !inTryBlock) { issues.push({ category: types_2.TechnicalDebtCategory.MISSING_ERROR_HANDLING, severity: types_2.IssueSeverity.HIGH, location: { filePath: file.filePath, line: index + 1, context: trimmed.substring(0, 100), }, description: 'File operation without error handling', recommendation: 'Wrap file operations in try/catch blocks.', }); } // Detect JSON.parse without error handling if (trimmed.includes('JSON.parse(') && !inTryBlock) { issues.push({ category: types_2.TechnicalDebtCategory.MISSING_ERROR_HANDLING, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: index + 1, context: trimmed.substring(0, 100), }, description: 'JSON.parse without error handling', recommendation: 'Wrap JSON.parse in try/catch to handle malformed JSON.', }); } }); } catch (error) { // File read error - skip } }); return issues; } /** * Detect deprecated API usage * * DESIGN DECISION: Pattern-based detection for common deprecations * WHY: Language-specific deprecation lists too large to maintain */ detectDeprecatedAPIs(parseResult) { const issues = []; // Common deprecated APIs (expandable) const deprecatedPatterns = [ { pattern: /new Buffer\(/, replacement: 'Buffer.from() or Buffer.alloc()' }, { pattern: /require\(['"]domain['"]\)/, replacement: 'Use async_hooks or promise error handling' }, { pattern: /url\.parse\(/, replacement: 'new URL()' }, { pattern: /crypto\.createCipher\(/, replacement: 'crypto.createCipheriv()' }, ]; parseResult.files.forEach((file) => { try { const content = fs.readFileSync(file.filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { deprecatedPatterns.forEach((deprecated) => { if (line.match(deprecated.pattern)) { issues.push({ category: types_2.TechnicalDebtCategory.DEPRECATED_API, severity: types_2.IssueSeverity.MEDIUM, location: { filePath: file.filePath, line: index + 1, context: line.trim().substring(0, 100), }, description: 'Deprecated API usage detected', recommendation: `Replace with: ${deprecated.replacement}`, }); } }); }); } catch (error) { // File read error - skip } }); return issues; } /** * Calculate overall debt score (0-100) * * DESIGN DECISION: Weighted scoring by severity * WHY: High severity issues should dominate score * * Score = (high × 10 + medium × 5 + low × 1) / (total_files × 10) * Capped at 100 */ calculateDebtScore(issues) { const high = issues.filter((i) => i.severity === types_2.IssueSeverity.HIGH).length; const medium = issues.filter((i) => i.severity === types_2.IssueSeverity.MEDIUM).length; const low = issues.filter((i) => i.severity === types_2.IssueSeverity.LOW).length; const weightedScore = high * 10 + medium * 5 + low * 1; const maxScore = 100; // Normalize to 0-100 const normalizedScore = Math.min((weightedScore / maxScore) * 100, 100); return Math.round(normalizedScore); } /** * Categorize issues by type */ categorizeIssues(issues) { const categories = {}; issues.forEach((issue) => { const category = issue.category; categories[category] = (categories[category] || 0) + 1; }); return categories; } /** * Generate summary report */ generateSummaryReport(analysis) { let report = '# Technical Debt Analysis Report\n\n'; // Overall score report += '## Debt Score\n\n'; report += `**Overall Score:** ${analysis.score}/100\n\n`; if (analysis.score > 70) { report += '⚠️ **Status:** Critical - High technical debt requires immediate attention\n\n'; } else if (analysis.score > 40) { report += '⚠️ **Status:** Moderate - Technical debt should be addressed in upcoming sprints\n\n'; } else { report += '✅ **Status:** Low - Manageable technical debt\n\n'; } // Statistics report += '## Statistics\n\n'; report += `- **Total Issues:** ${analysis.totalIssues}\n`; report += `- **High Priority:** ${analysis.highPriority}\n`; report += `- **Medium Priority:** ${analysis.mediumPriority}\n`; report += `- **Low Priority:** ${analysis.lowPriority}\n\n`; // Categories if (Object.keys(analysis.categories).length > 0) { report += '## Issues by Category\n\n'; report += '| Category | Count |\n'; report += '|----------|-------|\n'; Object.entries(analysis.categories) .sort((a, b) => b[1] - a[1]) .forEach(([category, count]) => { report += `| ${category.replace(/_/g, ' ')} | ${count} |\n`; }); report += '\n'; } // Top issues const topIssues = analysis.issues .filter((i) => i.severity === types_2.IssueSeverity.HIGH) .slice(0, 10); if (topIssues.length > 0) { report += '## High Priority Issues (Top 10)\n\n'; report += '| File | Line | Issue | Recommendation |\n'; report += '|------|------|-------|----------------|\n'; topIssues.forEach((issue) => { const fileName = issue.location.filePath.split(/[\\/]/).pop(); report += `| ${fileName} | ${issue.location.line} | ${issue.description} | ${issue.recommendation} |\n`; }); report += '\n'; } // Recommendations report += '## Recommendations\n\n'; if (analysis.highPriority > 0) { report += `1. **Immediate Action:** Address ${analysis.highPriority} high-priority issues (HACKs, secrets, god classes)\n`; } if (analysis.mediumPriority > 0) { report += `2. **Sprint Planning:** Include ${analysis.mediumPriority} medium-priority issues in upcoming sprints (FIXMEs, long methods, hardcoded URLs)\n`; } if (analysis.lowPriority > 0) { report += `3. **Continuous Improvement:** Gradually address ${analysis.lowPriority} low-priority issues (TODOs, magic numbers)\n`; } return report; } } exports.TechnicalDebtAnalyzer = TechnicalDebtAnalyzer; //# sourceMappingURL=technical-debt-analyzer.js.map