mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
474 lines • 20.4 kB
JavaScript
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