@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
838 lines • 32.5 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
export class CodeAnalyzer {
knownPatterns;
constructor() {
this.knownPatterns = this.initializePatterns();
}
async findCodeFiles(directory, excludePatterns = []) {
const files = [];
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(directory, fullPath);
// Check if should exclude
if (excludePatterns.some(pattern => {
// Simple glob matching
const regex = pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '.');
return new RegExp(`^${regex}$`).test(relativePath);
})) {
continue;
}
if (entry.isDirectory()) {
await walk(fullPath);
}
else if (entry.isFile() && isCodeFile(entry.name)) {
files.push(fullPath);
}
}
}
function isCodeFile(name) {
const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.cs', '.rb', '.go', '.rust', '.swift', '.php'];
return codeExtensions.some(ext => name.endsWith(ext));
}
await walk(directory);
return files;
}
generateSuggestions(analysis) {
const suggestions = [];
// Complexity suggestions
if (analysis.metrics.complexity.average > 10) {
suggestions.push({
title: 'Reduce complexity',
description: 'Consider breaking down complex functions into smaller, more manageable pieces',
priority: 'high',
});
}
// Maintainability suggestions
if (analysis.metrics.maintainability.average < 70) {
suggestions.push({
title: 'Improve maintainability',
description: 'Add documentation, reduce complexity, and improve code organization',
priority: 'medium',
});
}
// Duplication suggestions
if (analysis.metrics.duplication.percentage > 5) {
suggestions.push({
title: 'Reduce code duplication',
description: 'Extract common code into reusable functions or modules',
priority: 'medium',
});
}
return suggestions;
}
generateRefactoringIdeas(analysis) {
const ideas = [];
// Check for long functions
const longFunctions = analysis.metrics.complexity.functions.filter(f => f.lines > 50);
if (longFunctions.length > 0) {
ideas.push({
title: 'Split long functions',
description: `${longFunctions.length} functions exceed 50 lines. Consider splitting them`,
priority: 'medium',
});
}
// Check for high parameter count
const highParamFunctions = analysis.metrics.complexity.functions.filter(f => f.parameters > 5);
if (highParamFunctions.length > 0) {
ideas.push({
title: 'Reduce parameter count',
description: `${highParamFunctions.length} functions have more than 5 parameters. Consider using objects or builder patterns`,
priority: 'low',
});
}
return ideas;
}
async analyzeFile(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
const stats = await fs.stat(filePath);
const language = this.detectLanguage(filePath);
const metrics = await this.calculateMetrics(content, language);
const issues = await this.detectIssues(content, filePath, language);
return {
path: filePath,
metrics,
issues,
size: stats.size,
language,
lastModified: stats.mtime.toISOString(),
};
}
async calculateMetrics(content, language) {
const complexity = this.calculateComplexity(content, language);
const maintainability = this.calculateMaintainability(content, complexity);
const duplication = this.detectDuplication(content);
const quality = await this.assessQuality(content, complexity, maintainability, duplication);
return {
complexity,
maintainability,
duplication,
quality,
};
}
calculateComplexity(content, language) {
const functions = this.extractFunctions(content, language);
const cyclomaticComplexity = this.calculateCyclomaticComplexity(content);
const cognitiveComplexity = this.calculateCognitiveComplexity(content);
const halstead = this.calculateHalsteadMetrics(content);
const average = functions.length > 0
? functions.reduce((sum, f) => sum + f.complexity, 0) / functions.length
: cyclomaticComplexity;
const max = functions.length > 0
? Math.max(...functions.map(f => f.complexity))
: cyclomaticComplexity;
return {
cyclomatic: cyclomaticComplexity,
cognitive: cognitiveComplexity,
halstead,
functions,
average,
max,
total: cyclomaticComplexity,
};
}
calculateCyclomaticComplexity(content) {
// Count decision points
const patterns = [
/\bif\b/g,
/\belse\s+if\b/g,
/\belse\b/g,
/\bfor\b/g,
/\bwhile\b/g,
/\bdo\b/g,
/\bswitch\b/g,
/\bcase\b/g,
/\bcatch\b/g,
/\?\s*[^:]+\s*:/g, // ternary
/&&/g,
/\|\|/g,
/\?\?/g, // nullish coalescing
];
let complexity = 1; // Base complexity
patterns.forEach(pattern => {
const matches = content.match(pattern);
if (matches) {
complexity += matches.length;
}
});
// Adjust for certain patterns
const elseIfMatches = content.match(/\belse\s+if\b/g);
if (elseIfMatches) {
// Don't double count else if
complexity -= elseIfMatches.length;
}
return complexity;
}
calculateCognitiveComplexity(content) {
let complexity = 0;
let nestingLevel = 0;
const lines = content.split('\n');
const incrementPatterns = [
/\bif\b/,
/\belse\s+if\b/,
/\belse\b/,
/\bfor\b/,
/\bwhile\b/,
/\bdo\b/,
/\bcatch\b/,
];
const nestingPatterns = [
/\bif\b/,
/\bfor\b/,
/\bwhile\b/,
/\bdo\b/,
];
lines.forEach(line => {
const trimmed = line.trim();
// Check for nesting increase
nestingPatterns.forEach(pattern => {
if (pattern.test(trimmed) && trimmed.includes('{')) {
nestingLevel++;
}
});
// Check for complexity increment
incrementPatterns.forEach(pattern => {
if (pattern.test(trimmed)) {
complexity += 1 + nestingLevel;
}
});
// Check for nesting decrease
if (trimmed.includes('}')) {
nestingLevel = Math.max(0, nestingLevel - 1);
}
});
return complexity;
}
calculateHalsteadMetrics(content) {
// Simplified Halstead metrics calculation
const operators = new Set();
const operands = new Set();
let totalOperators = 0;
let totalOperands = 0;
// Extract operators
const operatorPatterns = [
/[+\-*/%=<>!&|^~]/g,
/\b(if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally)\b/g,
];
operatorPatterns.forEach(pattern => {
const matches = content.match(pattern);
if (matches) {
matches.forEach(match => {
operators.add(match);
totalOperators++;
});
}
});
// Extract operands (simplified - identifiers and literals)
const operandPattern = /\b[a-zA-Z_]\w*\b|\b\d+\b|"[^"]*"|'[^']*'/g;
const operandMatches = content.match(operandPattern);
if (operandMatches) {
operandMatches.forEach(match => {
if (!operators.has(match)) {
operands.add(match);
totalOperands++;
}
});
}
const n1 = operators.size; // unique operators
const n2 = operands.size; // unique operands
const N1 = totalOperators; // total operators
const N2 = totalOperands; // total operands
const vocabulary = n1 + n2;
const length = N1 + N2;
const volume = length * Math.log2(vocabulary || 1);
const difficulty = (n1 / 2) * (N2 / (n2 || 1));
const effort = difficulty * volume;
return {
difficulty: Math.round(difficulty * 100) / 100,
volume: Math.round(volume * 100) / 100,
effort: Math.round(effort * 100) / 100,
};
}
extractFunctions(content, language) {
const functions = [];
// Patterns for different languages
const patterns = {
javascript: /(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>|(\w+)\s*:\s*(?:async\s*)?\([^)]*\)\s*=>)/g,
typescript: /(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>|(\w+)\s*:\s*(?:async\s*)?\([^)]*\)\s*=>|(?:public|private|protected)\s+(?:async\s+)?(\w+)\s*\([^)]*\))/g,
python: /def\s+(\w+)\s*\([^)]*\):/g,
java: /(?:public|private|protected)?\s*(?:static)?\s*(?:\w+)\s+(\w+)\s*\([^)]*\)\s*{/g,
};
const pattern = patterns[language] || patterns.javascript;
const lines = content.split('\n');
let match;
while ((match = pattern.exec(content)) !== null) {
const functionName = match[1] || match[2] || match[3] || match[4] || 'anonymous';
const startIndex = match.index;
const lineNumber = content.substring(0, startIndex).split('\n').length;
// Extract function body (simplified)
const functionStart = startIndex;
const functionBody = this.extractFunctionBody(content, functionStart, language);
const functionLines = functionBody.split('\n').length;
// Calculate function-specific complexity
const functionComplexity = this.calculateCyclomaticComplexity(functionBody);
// Count parameters (simplified)
const paramsMatch = match[0].match(/\(([^)]*)\)/);
const parameters = paramsMatch && paramsMatch[1].trim()
? paramsMatch[1].split(',').length
: 0;
functions.push({
name: functionName,
complexity: functionComplexity,
lines: functionLines,
parameters,
location: {
file: '',
line: lineNumber,
column: 1,
},
});
}
return functions;
}
extractFunctionBody(content, startIndex, language) {
// Simplified function body extraction
if (language === 'python') {
// Python uses indentation
const lines = content.substring(startIndex).split('\n');
const functionLines = [lines[0]];
const baseIndent = lines[0].match(/^\s*/)?.[0].length || 0;
for (let i = 1; i < lines.length; i++) {
const currentIndent = lines[i].match(/^\s*/)?.[0].length || 0;
if (currentIndent > baseIndent || lines[i].trim() === '') {
functionLines.push(lines[i]);
}
else {
break;
}
}
return functionLines.join('\n');
}
else {
// Brace-based languages
let braceCount = 0;
let inFunction = false;
let functionEnd = startIndex;
for (let i = startIndex; i < content.length; i++) {
if (content[i] === '{') {
braceCount++;
inFunction = true;
}
else if (content[i] === '}') {
braceCount--;
if (inFunction && braceCount === 0) {
functionEnd = i + 1;
break;
}
}
}
return content.substring(startIndex, functionEnd);
}
}
calculateMaintainability(content, complexity) {
const lines = content.split('\n');
const totalLines = lines.length;
const codeLines = lines.filter(line => line.trim() && !line.trim().startsWith('//')).length;
const commentLines = lines.filter(line => line.trim().startsWith('//') || line.trim().startsWith('/*')).length;
const commentRatio = commentLines / (codeLines || 1);
// Simplified maintainability index calculation
// MI = 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code)
const volume = complexity.halstead.volume || 1;
const cyclomatic = complexity.cyclomatic || 1;
let maintainabilityIndex = 171
- 5.2 * Math.log(volume)
- 0.23 * cyclomatic
- 16.2 * Math.log(codeLines || 1);
// Normalize to 0-100
maintainabilityIndex = Math.max(0, Math.min(100, maintainabilityIndex));
// Determine rating
let rating;
if (maintainabilityIndex >= 80)
rating = 'A';
else if (maintainabilityIndex >= 60)
rating = 'B';
else if (maintainabilityIndex >= 40)
rating = 'C';
else if (maintainabilityIndex >= 20)
rating = 'D';
else
rating = 'F';
return {
index: Math.round(maintainabilityIndex * 100) / 100,
rating,
factors: {
complexity: complexity.cyclomatic,
lineCount: codeLines,
commentRatio: Math.round(commentRatio * 100) / 100,
},
average: Math.round(maintainabilityIndex * 100) / 100,
min: Math.round(maintainabilityIndex * 100) / 100,
distribution: {},
};
}
detectDuplication(content) {
const lines = content.split('\n');
const totalLines = lines.length;
const blockSize = 6; // Minimum duplicate block size
const hashes = new Map();
const duplicateBlocks = [];
// Create hashes for each block of lines
for (let i = 0; i <= lines.length - blockSize; i++) {
const block = lines.slice(i, i + blockSize).join('\n');
const hash = this.hashBlock(block);
if (!hashes.has(hash)) {
hashes.set(hash, []);
}
hashes.get(hash).push(i);
}
// Find duplicate blocks
let duplicatedLines = 0;
const processedLines = new Set();
hashes.forEach((locations, hash) => {
if (locations.length > 1) {
const block = {
locations: locations.map(line => ({
file: '',
line: line + 1,
column: 1,
endLine: line + blockSize,
})),
lines: blockSize,
tokens: blockSize * 10, // Approximation
hash,
};
duplicateBlocks.push(block);
// Count duplicated lines (avoid double counting)
locations.forEach(loc => {
for (let i = loc; i < loc + blockSize; i++) {
if (!processedLines.has(i)) {
processedLines.add(i);
duplicatedLines++;
}
}
});
}
});
const percentage = (duplicatedLines / totalLines) * 100;
return {
percentage: Math.round(percentage * 100) / 100,
duplicateBlocks,
totalLines,
duplicatedLines,
blocks: duplicateBlocks.length,
};
}
hashBlock(block) {
// Normalize whitespace and create hash
const normalized = block
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('//'))
.join('\n');
return crypto.createHash('md5').update(normalized).digest('hex');
}
async assessQuality(content, complexity, maintainability, duplication) {
const issues = await this.detectIssues(content, '', this.detectLanguage(''));
// Calculate quality score
let score = 100;
// Deduct for complexity
if (complexity.cyclomatic > 10)
score -= 10;
if (complexity.cyclomatic > 20)
score -= 10;
// Deduct for maintainability
if (maintainability.rating === 'B')
score -= 5;
if (maintainability.rating === 'C')
score -= 10;
if (maintainability.rating === 'D')
score -= 20;
if (maintainability.rating === 'F')
score -= 30;
// Deduct for duplication
if (duplication.percentage > 5)
score -= 5;
if (duplication.percentage > 10)
score -= 10;
if (duplication.percentage > 20)
score -= 20;
// Deduct for issues
issues.forEach(issue => {
if (issue.severity === 'critical')
score -= 10;
else if (issue.severity === 'high')
score -= 5;
else if (issue.severity === 'medium')
score -= 2;
else if (issue.severity === 'low')
score -= 1;
});
score = Math.max(0, score);
// Determine grade
let grade;
if (score >= 90)
grade = 'A';
else if (score >= 80)
grade = 'B';
else if (score >= 70)
grade = 'C';
else if (score >= 60)
grade = 'D';
else
grade = 'F';
const issuesSummary = {
total: issues.length,
high: issues.filter(i => i.severity === 'high').length,
medium: issues.filter(i => i.severity === 'medium').length,
low: issues.filter(i => i.severity === 'low').length,
};
return {
issues: issuesSummary,
codeSmells: issues.filter(i => i.category === 'maintainability').length,
technicalDebt: Math.round(issues.length * 0.5),
score,
grade,
};
}
async detectIssues(content, filePath, language) {
const issues = [];
const lines = content.split('\n');
// Security issues
this.detectSecurityIssues(lines, issues);
// Performance issues
this.detectPerformanceIssues(lines, issues);
// Code quality issues
this.detectQualityIssues(lines, issues);
// Style issues
this.detectStyleIssues(lines, issues);
return issues;
}
detectSecurityIssues(lines, issues) {
const securityPatterns = [
{
pattern: /eval\s*\(/,
message: 'Avoid using eval() as it can execute arbitrary code',
severity: 'critical',
rule: 'no-eval',
},
{
pattern: /innerHTML\s*=/,
message: 'Direct innerHTML assignment can lead to XSS vulnerabilities',
severity: 'high',
rule: 'no-inner-html',
},
{
pattern: /password.*=.*["'][^"']+["']/i,
message: 'Hardcoded password detected',
severity: 'critical',
rule: 'no-hardcoded-secrets',
},
{
pattern: /api[_-]?key.*=.*["'][^"']+["']/i,
message: 'Hardcoded API key detected',
severity: 'critical',
rule: 'no-hardcoded-secrets',
},
];
lines.forEach((line, index) => {
securityPatterns.forEach(({ pattern, message, severity, rule }) => {
if (pattern.test(line)) {
issues.push({
type: 'error',
severity,
rule,
message,
location: {
file: '',
line: index + 1,
column: line.search(pattern) + 1,
},
fixable: false,
category: 'security',
});
}
});
});
}
detectPerformanceIssues(lines, issues) {
const performancePatterns = [
{
pattern: /\.forEach\s*\([^)]*\)\s*{[^}]*\.push\s*\(/,
message: 'Consider using map() instead of forEach() with push()',
severity: 'medium',
rule: 'prefer-map',
},
{
pattern: /for\s*\([^)]*in\s+/,
message: 'for...in loop can be slow for arrays, consider for...of or traditional for loop',
severity: 'low',
rule: 'no-for-in',
},
];
lines.forEach((line, index) => {
performancePatterns.forEach(({ pattern, message, severity, rule }) => {
if (pattern.test(line)) {
issues.push({
type: 'warning',
severity,
rule,
message,
location: {
file: '',
line: index + 1,
column: 1,
},
fixable: true,
category: 'performance',
});
}
});
});
}
detectQualityIssues(lines, issues) {
lines.forEach((line, index) => {
// Long lines
if (line.length > 120) {
issues.push({
type: 'warning',
severity: 'low',
rule: 'max-line-length',
message: `Line exceeds maximum length of 120 characters (${line.length})`,
location: {
file: '',
line: index + 1,
column: 121,
},
fixable: true,
category: 'style',
});
}
// TODO comments
if (/\/\/\s*TODO|\/\*\s*TODO/.test(line)) {
issues.push({
type: 'info',
severity: 'low',
rule: 'no-todo',
message: 'TODO comment found',
location: {
file: '',
line: index + 1,
column: line.search(/TODO/) + 1,
},
fixable: false,
category: 'maintainability',
});
}
// Console.log statements
if (/console\.(log|error|warn|info)/.test(line)) {
issues.push({
type: 'warning',
severity: 'medium',
rule: 'no-console',
message: 'Remove console statements before production',
location: {
file: '',
line: index + 1,
column: line.search(/console\./) + 1,
},
fixable: true,
category: 'reliability',
});
}
});
}
detectStyleIssues(lines, issues) {
lines.forEach((line, index) => {
// Trailing whitespace
if (/\s+$/.test(line)) {
issues.push({
type: 'style',
severity: 'low',
rule: 'no-trailing-spaces',
message: 'Trailing whitespace',
location: {
file: '',
line: index + 1,
column: line.search(/\s+$/) + 1,
},
fixable: true,
category: 'style',
});
}
// Mixed tabs and spaces
if (/^\t+ /.test(line) || /^ +\t/.test(line)) {
issues.push({
type: 'style',
severity: 'low',
rule: 'no-mixed-spaces-and-tabs',
message: 'Mixed spaces and tabs',
location: {
file: '',
line: index + 1,
column: 1,
},
fixable: true,
category: 'style',
});
}
});
}
detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const languageMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.c': 'c',
'.cpp': 'cpp',
'.cs': 'csharp',
'.go': 'go',
'.rb': 'ruby',
'.php': 'php',
'.swift': 'swift',
'.kt': 'kotlin',
'.rs': 'rust',
};
return languageMap[ext] || 'unknown';
}
initializePatterns() {
const patterns = new Map();
// Add common patterns for various languages
patterns.set('function-declaration-js', /function\s+\w+\s*\([^)]*\)\s*{/g);
patterns.set('arrow-function-js', /\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g);
patterns.set('class-declaration-js', /class\s+\w+(?:\s+extends\s+\w+)?\s*{/g);
return patterns;
}
async analyzeDirectory(dirPath, options = {}) {
const files = await this.getFilesRecursively(dirPath, options.excludePatterns);
const fileAnalyses = [];
for (const file of files) {
try {
const analysis = await this.analyzeFile(file);
fileAnalyses.push(analysis);
}
catch (error) {
console.error(`Error analyzing ${file}:`, error);
}
}
const summary = this.generateSummary(fileAnalyses);
const recommendations = this.generateRecommendations(summary);
return {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
files: fileAnalyses,
summary,
recommendations,
};
}
async getFilesRecursively(dirPath, excludePatterns = []) {
const files = [];
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
// Check if should exclude
if (excludePatterns.some(pattern => fullPath.includes(pattern))) {
continue;
}
if (entry.isDirectory()) {
// Skip common directories
if (['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
continue;
}
const subFiles = await this.getFilesRecursively(fullPath, excludePatterns);
files.push(...subFiles);
}
else if (entry.isFile()) {
// Only include source files
if (this.isSourceFile(entry.name)) {
files.push(fullPath);
}
}
}
return files;
}
isSourceFile(fileName) {
const sourceExtensions = [
'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp',
'.cs', '.go', '.rb', '.php', '.swift', '.kt', '.rs'
];
return sourceExtensions.some(ext => fileName.endsWith(ext));
}
generateSummary(files) {
if (files.length === 0) {
return {
totalFiles: 0,
totalLines: 0,
averageComplexity: 0,
averageMaintainability: 0,
totalIssues: 0,
criticalIssues: 0,
duplicatePercentage: 0,
overallScore: 0,
overallGrade: 'F',
};
}
const totalFiles = files.length;
const totalLines = files.reduce((sum, f) => sum + f.metrics.duplication.totalLines, 0);
const totalComplexity = files.reduce((sum, f) => sum + f.metrics.complexity.cyclomatic, 0);
const totalMaintainability = files.reduce((sum, f) => sum + f.metrics.maintainability.index, 0);
const totalIssues = files.reduce((sum, f) => sum + f.issues.length, 0);
const criticalIssues = files.reduce((sum, f) => sum + f.issues.filter(i => i.severity === 'critical').length, 0);
const totalDuplicatedLines = files.reduce((sum, f) => sum + f.metrics.duplication.duplicatedLines, 0);
const overallScore = files.reduce((sum, f) => sum + f.metrics.quality.score, 0) / totalFiles;
let overallGrade;
if (overallScore >= 90)
overallGrade = 'A';
else if (overallScore >= 80)
overallGrade = 'B';
else if (overallScore >= 70)
overallGrade = 'C';
else if (overallScore >= 60)
overallGrade = 'D';
else
overallGrade = 'F';
return {
totalFiles,
totalLines,
averageComplexity: Math.round((totalComplexity / totalFiles) * 100) / 100,
averageMaintainability: Math.round((totalMaintainability / totalFiles) * 100) / 100,
totalIssues,
criticalIssues,
duplicatePercentage: Math.round((totalDuplicatedLines / totalLines) * 100 * 100) / 100,
overallScore: Math.round(overallScore * 100) / 100,
overallGrade,
};
}
generateRecommendations(summary) {
const recommendations = [];
if (summary.averageComplexity > 10) {
recommendations.push('Consider refactoring complex functions to reduce cyclomatic complexity');
}
if (summary.averageMaintainability < 60) {
recommendations.push('Improve code maintainability by reducing complexity and adding documentation');
}
if (summary.criticalIssues > 0) {
recommendations.push(`Address ${summary.criticalIssues} critical security/quality issues immediately`);
}
if (summary.duplicatePercentage > 10) {
recommendations.push('Reduce code duplication by extracting common functionality');
}
if (summary.overallGrade === 'D' || summary.overallGrade === 'F') {
recommendations.push('Consider a comprehensive code quality improvement initiative');
}
if (recommendations.length === 0) {
recommendations.push('Code quality is good! Consider adding more tests and documentation');
}
return recommendations;
}
}
//# sourceMappingURL=analyzer.js.map