UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

474 lines 20.4 kB
import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; export class CodeQualityAnalyzer { projectRoot; constructor(projectRoot) { this.projectRoot = projectRoot; } async quickScan() { const issues = []; const suggestions = []; // Check for basic quality indicators const hasLinter = await this.checkForLinter(); const hasFormatter = await this.checkForFormatter(); const hasEditorConfig = await fs.pathExists(path.join(this.projectRoot, '.editorconfig')); if (!hasLinter) { issues.push({ file: 'project', severity: 'warning', message: 'No linter configuration found', rule: 'project-setup' }); suggestions.push('Consider adding ESLint, TSLint, or another linter'); } if (!hasFormatter) { issues.push({ file: 'project', severity: 'info', message: 'No code formatter configuration found', rule: 'project-setup' }); suggestions.push('Consider adding Prettier or another code formatter'); } // Quick file checks const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: false }); // Sample a few files for quick analysis const samplesToCheck = files.slice(0, 10); for (const file of samplesToCheck) { await this.analyzeFile(path.join(this.projectRoot, file), issues); } // Calculate score const score = this.calculateScore(issues); return { score, overallScore: score, // For compatibility issues: issues.slice(0, 20), // Limit issues for quick scan suggestions }; } async fullAnalysis() { const issues = []; const suggestions = []; // Comprehensive checks await this.checkProjectStructure(issues, suggestions); await this.checkCodePatterns(issues, suggestions); await this.checkDependencies(issues, suggestions); // Advanced analysis const duplicationAnalysis = await this.analyzeDuplication(issues); const maintainabilityIndex = await this.calculateMaintainabilityIndex(issues); const technicalDebt = await this.calculateTechnicalDebt(issues); const score = this.calculateScore(issues, duplicationAnalysis, maintainabilityIndex, technicalDebt); return { score, overallScore: score, // For compatibility issues, suggestions, duplicationAnalysis, maintainabilityIndex, technicalDebt }; } async analyze() { // Default to quick scan for standard analyze() method return this.quickScan(); } async checkForLinter() { const linterFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', 'tslint.json']; for (const file of linterFiles) { if (await fs.pathExists(path.join(this.projectRoot, file))) { return true; } } // Check package.json for eslint config const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); if (packageJson.eslintConfig) { return true; } } return false; } async checkForFormatter() { const formatterFiles = ['.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml']; for (const file of formatterFiles) { if (await fs.pathExists(path.join(this.projectRoot, file))) { return true; } } return false; } async analyzeFile(filePath, issues) { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const relativePath = path.relative(this.projectRoot, filePath); // Check for very long lines lines.forEach((line, index) => { if (line.length > 120) { issues.push({ file: relativePath, line: index + 1, severity: 'warning', message: `Line too long (${line.length} characters)`, rule: 'max-line-length' }); } }); // Check for console.log statements if (content.includes('console.log')) { issues.push({ file: relativePath, severity: 'warning', message: 'Contains console.log statements', rule: 'no-console' }); } // Check for TODO comments if (content.match(/\/\/\s*(TODO|FIXME|HACK)/i)) { issues.push({ file: relativePath, severity: 'info', message: 'Contains TODO/FIXME comments', rule: 'no-todo' }); } } catch (error) { // Skip files that can't be analyzed } } async checkProjectStructure(issues, suggestions) { // Check for README if (!await fs.pathExists(path.join(this.projectRoot, 'README.md'))) { issues.push({ file: 'project', severity: 'warning', message: 'No README.md found', rule: 'documentation' }); suggestions.push('Add a README.md file to document your project'); } // Check for LICENSE if (!await fs.pathExists(path.join(this.projectRoot, 'LICENSE'))) { issues.push({ file: 'project', severity: 'info', message: 'No LICENSE file found', rule: 'licensing' }); } } async checkCodePatterns(issues, suggestions) { // Get all files but limit analysis to reasonable number const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'], absolute: false }); // Limit to 50 files max for full analysis to prevent performance issues const filesToAnalyze = files.slice(0, 50); for (const file of filesToAnalyze) { await this.analyzeFile(path.join(this.projectRoot, file), issues); } if (files.length > 50) { suggestions.push(`Analysis limited to ${filesToAnalyze.length} of ${files.length} files for performance. Consider using quickScan for lighter analysis.`); } } async checkDependencies(issues, suggestions) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); // Check for outdated dependencies (would require npm API) if (!packageJson.scripts?.['check-updates']) { suggestions.push('Consider adding npm-check-updates to monitor dependency versions'); } } } async analyzeDuplication(issues) { try { const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'], absolute: false }); // Limit duplication analysis to 100 files max for performance const filesToAnalyze = files.slice(0, 100); const codeBlocks = new Map(); let totalLines = 0; let duplicatedLines = 0; for (const file of filesToAnalyze) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); totalLines += lines.length; // Analyze blocks of 5+ lines for (let i = 0; i <= lines.length - 5; i++) { const block = lines.slice(i, i + 5); const normalizedBlock = this.normalizeCodeBlock(block); if (this.isSignificantBlock(normalizedBlock)) { const hash = this.hashCode(normalizedBlock); if (!codeBlocks.has(hash)) { codeBlocks.set(hash, { files: [], lines: 5, content: normalizedBlock }); } const existing = codeBlocks.get(hash); if (!existing.files.includes(file)) { existing.files.push(file); } } } } catch (error) { // Skip files that can't be read } } const duplicatedBlocks = []; for (const [hash, block] of codeBlocks.entries()) { if (block.files.length > 1) { duplicatedBlocks.push({ hash, occurrences: block.files.length, files: block.files, lines: block.lines, similarity: 1.0 // Exact match for now }); duplicatedLines += block.lines * (block.files.length - 1); issues.push({ file: block.files.join(', '), severity: 'warning', message: `Duplicated code block found in ${block.files.length} files`, rule: 'no-duplicated-code' }); } } const duplicationPercentage = totalLines > 0 ? (duplicatedLines / totalLines) * 100 : 0; return { duplicatedLines, totalLines, duplicationPercentage, duplicatedBlocks: duplicatedBlocks.slice(0, 20) // Limit to top 20 }; } catch (error) { return undefined; } } async calculateMaintainabilityIndex(issues) { try { const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'], absolute: false }); // Limit maintainability analysis to 75 files max for performance const filesToAnalyze = files.slice(0, 75); let totalComplexity = 0; let totalVolume = 0; let fileCount = 0; const recommendations = []; for (const file of filesToAnalyze) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n').filter(line => line.trim().length > 0); // Calculate Halstead volume (simplified) const operators = content.match(/[+\-*/%=<>!&|?:;,.()\[\]{}]/g) || []; const operands = content.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g) || []; const vocabulary = new Set([...operators, ...operands]).size; const volume = (operators.length + operands.length) * Math.log2(vocabulary || 1); // Calculate cyclomatic complexity (simplified) const complexityPatterns = [ /\bif\b/g, /\belse\b/g, /\bwhile\b/g, /\bfor\b/g, /\bcase\b/g, /\bcatch\b/g, /\?\s*:/g, /&&/g, /\|\|/g ]; let complexity = 1; // Base complexity for (const pattern of complexityPatterns) { const matches = content.match(pattern); if (matches) complexity += matches.length; } totalComplexity += complexity; totalVolume += volume; fileCount++; // Add specific recommendations if (complexity > 20) { recommendations.push(`${file}: High complexity (${complexity}) - consider refactoring`); } if (lines.length > 500) { recommendations.push(`${file}: Large file (${lines.length} lines) - consider splitting`); } } catch (error) { // Skip files that can't be read } } if (fileCount === 0) return undefined; const avgComplexity = totalComplexity / fileCount; const avgVolume = totalVolume / fileCount; // Maintainability Index calculation (simplified version of Microsoft's formula) const maintainabilityIndexRaw = Math.max(0, 171 - 5.2 * Math.log(avgVolume) - 0.23 * avgComplexity - 16.2 * Math.log(avgVolume / 100)); const overallScore = Math.min(100, maintainabilityIndexRaw); let maintainability; if (overallScore >= 85) maintainability = 'excellent'; else if (overallScore >= 70) maintainability = 'good'; else if (overallScore >= 50) maintainability = 'fair'; else maintainability = 'poor'; if (maintainability === 'poor') { issues.push({ file: 'project', severity: 'warning', message: `Low maintainability index (${overallScore.toFixed(0)})`, rule: 'maintainability' }); } return { overallScore, complexity: avgComplexity, volume: avgVolume, maintainability, recommendations: recommendations.slice(0, 10) }; } catch (error) { return undefined; } } async calculateTechnicalDebt(issues) { try { const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'], absolute: false }); // Limit technical debt analysis to 75 files max for performance const filesToAnalyze = files.slice(0, 75); const highDebtFiles = []; let totalDebtMinutes = 0; let totalLines = 0; for (const file of filesToAnalyze) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); totalLines += lines.length; let fileDebtMinutes = 0; const fileIssues = []; // Technical debt indicators (realistic patterns only) const debtPatterns = [ { pattern: /\/\/\s*(TODO|FIXME|HACK|XXX)/gi, minutes: 15, name: 'TODO/FIXME comments' }, { pattern: /console\.(log|debug|info|warn|error)/g, minutes: 2, name: 'Console statements' }, { pattern: /debugger\s*;/g, minutes: 5, name: 'Debugger statements' }, { pattern: /:\s*any\b/g, minutes: 5, name: 'TypeScript any type usage' }, // Fixed: only actual type annotations { pattern: /eval\s*\(/g, minutes: 30, name: 'Eval usage' } // Removed: Block comments (not actually debt) // Removed: Long functions (too simplistic regex) ]; for (const debtPattern of debtPatterns) { const matches = content.match(debtPattern.pattern); if (matches) { const debt = matches.length * debtPattern.minutes; fileDebtMinutes += debt; if (matches.length > 0) { fileIssues.push(`${matches.length} ${debtPattern.name} (${debt} min)`); } } } if (fileDebtMinutes > 30) { // Files with significant debt highDebtFiles.push({ file, debtMinutes: fileDebtMinutes, issues: fileIssues }); } totalDebtMinutes += fileDebtMinutes; } catch (error) { // Skip files that can't be read } } const debtRatio = totalLines > 0 ? (totalDebtMinutes / totalLines) * 1000 : 0; // Debt per 1000 lines if (totalDebtMinutes > 100) { issues.push({ file: 'project', severity: 'warning', message: `High technical debt (${totalDebtMinutes} minutes)`, rule: 'technical-debt' }); } return { totalDebtMinutes, debtRatio, highDebtFiles: highDebtFiles.sort((a, b) => b.debtMinutes - a.debtMinutes).slice(0, 15) }; } catch (error) { return undefined; } } normalizeCodeBlock(lines) { return lines .map(line => line.trim()) .filter(line => line.length > 0 && !line.startsWith('//') && !line.startsWith('*')) .join('\n') .replace(/\s+/g, ' ') .toLowerCase(); } isSignificantBlock(block) { // Skip blocks that are too short or contain only simple statements return block.length > 20 && !block.includes('import ') && !block.includes('export ') && block.split(' ').length > 5; } hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash.toString(); } calculateScore(issues, duplicationAnalysis, maintainabilityIndex, technicalDebt) { let score = 100; // Deduct for issues for (const issue of issues) { switch (issue.severity) { case 'error': score -= 10; break; case 'warning': score -= 5; break; case 'info': score -= 1; break; } } // Deduct for code duplication if (duplicationAnalysis) { score -= duplicationAnalysis.duplicationPercentage * 0.5; } // Adjust for maintainability if (maintainabilityIndex) { score = (score + maintainabilityIndex.overallScore) / 2; } // Deduct for technical debt if (technicalDebt && technicalDebt.totalDebtMinutes > 100) { score -= Math.min(20, technicalDebt.totalDebtMinutes / 10); } return Math.max(0, Math.min(100, score)); } } //# sourceMappingURL=CodeQualityAnalyzer.js.map