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[];
}
}