// Copyright 2025 Chris Bunting
// Brief: Core static analysis service for MCP Server
// Scope: Orchestrates multi-language static analysis with various tools
import { LanguageDetector } from './LanguageDetector.js';
import { ConfigParser } from './ConfigParser.js';
import { Logger } from '../utils/Logger.js';
import {
AnalysisResult,
AnalysisIssue,
AnalysisMetrics,
AnalysisOptions,
SeverityLevel,
IssueCategory,
Language,
FixSuggestion,
} from '@mcp-code-analysis/shared-types';
import { glob } from 'fast-glob';
import { JavaScriptTypeScriptAnalyzer } from '../analyzers/JavaScriptTypeScriptAnalyzer.js';
import { PythonAnalyzer } from '../analyzers/PythonAnalyzer.js';
import { JavaAnalyzer } from '../analyzers/JavaAnalyzer.js';
import { CppAnalyzer } from '../analyzers/CppAnalyzer.js';
import { GoAnalyzer } from '../analyzers/GoAnalyzer.js';
import { RustAnalyzer } from '../analyzers/RustAnalyzer.js';
export interface AnalyzerInterface {
analyzeFile(filePath: string, options?: AnalysisOptions): Promise<AnalysisResult>;
analyzeProject(projectPath: string, patterns?: string[], excludePatterns?: string[], options?: AnalysisOptions): Promise<AnalysisResult[]>;
getRules(configFile?: string): Promise<any>;
configure(config: any): Promise<void>;
}
export class StaticAnalysisService {
private languageDetector: LanguageDetector;
private configParser: ConfigParser;
private logger: Logger;
private analyzers: Map<Language, AnalyzerInterface> = new Map();
constructor(
languageDetector: LanguageDetector,
configParser: ConfigParser,
logger: Logger
) {
this.languageDetector = languageDetector;
this.configParser = configParser;
this.logger = logger;
this.initializeAnalyzers();
}
private initializeAnalyzers(): void {
// Initialize language-specific analyzers
this.logger.info('Initializing analyzers for supported languages');
// Initialize JavaScript/TypeScript analyzer
this.analyzers.set(Language.JAVASCRIPT, new JavaScriptTypeScriptAnalyzer(this.logger, this.configParser));
this.analyzers.set(Language.TYPESCRIPT, new JavaScriptTypeScriptAnalyzer(this.logger, this.configParser));
// Initialize Python analyzer
this.analyzers.set(Language.PYTHON, new PythonAnalyzer(this.logger, this.configParser));
// Initialize Java analyzer
this.analyzers.set(Language.JAVA, new JavaAnalyzer(this.logger, this.configParser));
// Initialize C/C++ analyzers
this.analyzers.set(Language.C, new CppAnalyzer(this.logger, this.configParser));
this.analyzers.set(Language.CPP, new CppAnalyzer(this.logger, this.configParser));
// Initialize Go analyzer
this.analyzers.set(Language.GO, new GoAnalyzer(this.logger, this.configParser));
// Initialize Rust analyzer
this.analyzers.set(Language.RUST, new RustAnalyzer(this.logger, this.configParser));
this.logger.info(`Initialized ${this.analyzers.size} language analyzers`);
}
async analyzeFile(filePath: string, language?: string, options: AnalysisOptions = {}): Promise<AnalysisResult> {
try {
const detectedLanguage = language ? this.parseLanguage(language) : this.languageDetector.detectLanguage(filePath);
this.logger.info(`Analyzing file: ${filePath} (${detectedLanguage})`);
if (!this.analyzers.has(detectedLanguage)) {
throw new Error(`Unsupported language: ${detectedLanguage}`);
}
const analyzer = this.analyzers.get(detectedLanguage)!;
const result = await analyzer.analyzeFile(filePath, options);
// Enhance result with additional metadata
result.id = this.generateAnalysisId();
result.timestamp = new Date();
result.filePath = filePath;
result.language = detectedLanguage;
return result;
} catch (error) {
this.logger.error(`Error analyzing file ${filePath}:`, error);
throw this.createAnalysisError(filePath, error);
}
}
async analyzeProject(
projectPath: string,
filePatterns?: string[],
excludePatterns?: string[],
options: AnalysisOptions = {}
): Promise<AnalysisResult[]> {
try {
this.logger.info(`Analyzing project: ${projectPath}`);
const files = await this.discoverProjectFiles(projectPath, filePatterns, excludePatterns);
const results: AnalysisResult[] = [];
for (const file of files) {
try {
const result = await this.analyzeFile(file, undefined, options);
results.push(result);
} catch (error) {
this.logger.warn(`Failed to analyze file ${file}:`, error);
// Continue with other files
}
}
return results;
} catch (error) {
this.logger.error(`Error analyzing project ${projectPath}:`, error);
throw error;
}
}
async getRules(language: string, configFile?: string): Promise<any> {
try {
const detectedLanguage = this.parseLanguage(language);
this.logger.info(`Getting rules for language: ${detectedLanguage}`);
if (!this.analyzers.has(detectedLanguage)) {
throw new Error(`Unsupported language: ${detectedLanguage}`);
}
const analyzer = this.analyzers.get(detectedLanguage)!;
return await analyzer.getRules(configFile);
} catch (error) {
this.logger.error(`Error getting rules for ${language}:`, error);
throw error;
}
}
async configureAnalyzer(language: string, config: any): Promise<any> {
try {
const detectedLanguage = this.parseLanguage(language);
this.logger.info(`Configuring analyzer for language: ${detectedLanguage}`);
if (!this.analyzers.has(detectedLanguage)) {
throw new Error(`Unsupported language: ${detectedLanguage}`);
}
const analyzer = this.analyzers.get(detectedLanguage)!;
await analyzer.configure(config);
return { success: true, message: `Analyzer configured for ${detectedLanguage}` };
} catch (error) {
this.logger.error(`Error configuring analyzer for ${language}:`, error);
throw error;
}
}
async batchAnalyze(filePaths: string[], options: AnalysisOptions = {}): Promise<AnalysisResult[]> {
try {
this.logger.info(`Batch analyzing ${filePaths.length} files`);
const results: AnalysisResult[] = [];
const batchSize = 10; // Process files in batches to avoid overwhelming the system
for (let i = 0; i < filePaths.length; i += batchSize) {
const batch = filePaths.slice(i, i + batchSize);
const batchPromises = batch.map(filePath =>
this.analyzeFile(filePath, undefined, options).catch(error => {
this.logger.warn(`Failed to analyze file ${filePath}:`, error);
return null;
})
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults.filter((result): result is AnalysisResult => result !== null));
}
return results;
} catch (error) {
this.logger.error('Error in batch analysis:', error);
throw error;
}
}
private async discoverProjectFiles(
projectPath: string,
includePatterns?: string[],
excludePatterns?: string[]
): Promise<string[]> {
const defaultPatterns = [
'**/*.{js,jsx,ts,tsx,py,java,c,cpp,go,rs}',
'**/*.{h,hpp,cpp,cxx,cc}',
];
const defaultExcludePatterns = [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/target/**',
'**/.git/**',
'**/venv/**',
'**/env/**',
'**/__pycache__/**',
'**/*.min.js',
'**/*.bundle.js',
];
const patterns = includePatterns ?? defaultPatterns;
const exclude = excludePatterns ?? defaultExcludePatterns;
try {
const files = await glob(patterns, {
cwd: projectPath,
ignore: exclude,
absolute: true,
onlyFiles: true,
});
return files.sort();
} catch (error) {
this.logger.error('Error discovering project files:', error);
throw error;
}
}
private parseLanguage(language: string): Language {
const normalizedLanguage = language.toLowerCase();
switch (normalizedLanguage) {
case 'javascript':
case 'js':
return Language.JAVASCRIPT;
case 'typescript':
case 'ts':
return Language.TYPESCRIPT;
case 'python':
case 'py':
return Language.PYTHON;
case 'java':
return Language.JAVA;
case 'c':
return Language.C;
case 'cpp':
case 'c++':
return Language.CPP;
case 'go':
return Language.GO;
case 'rust':
case 'rs':
return Language.RUST;
default:
throw new Error(`Unsupported language: ${language}`);
}
}
private generateAnalysisId(): string {
return `analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private createAnalysisError(filePath: string, error: any): Error {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return new Error(`Analysis failed for ${filePath}: ${errorMessage}`);
}
// Helper methods for creating standardized analysis results
createAnalysisResult(
filePath: string,
language: Language,
issues: AnalysisIssue[],
metrics: AnalysisMetrics
): AnalysisResult {
return {
id: this.generateAnalysisId(),
timestamp: new Date(),
filePath,
language,
issues,
metrics,
severity: this.calculateOverallSeverity(issues),
};
}
createAnalysisIssue(
ruleId: string,
message: string,
severity: SeverityLevel,
line: number,
column: number,
category: IssueCategory,
fix?: FixSuggestion,
tags: string[] = []
): AnalysisIssue {
return {
id: this.generateIssueId(),
ruleId,
message,
severity,
line,
column,
category,
fix,
tags,
};
}
createAnalysisMetrics(
linesOfCode: number,
commentLines: number,
cyclomaticComplexity: number = 0,
cognitiveComplexity: number = 0,
maintainabilityIndex: number = 0
): AnalysisMetrics {
return {
linesOfCode,
commentLines,
cyclomaticComplexity,
cognitiveComplexity,
maintainabilityIndex,
};
}
private calculateOverallSeverity(issues: AnalysisIssue[]): SeverityLevel {
if (issues.length === 0) {
return SeverityLevel.INFO;
}
const hasErrors = issues.some(issue => issue.severity === SeverityLevel.ERROR);
const hasWarnings = issues.some(issue => issue.severity === SeverityLevel.WARNING);
if (hasErrors) {
return SeverityLevel.ERROR;
} else if (hasWarnings) {
return SeverityLevel.WARNING;
} else {
return SeverityLevel.INFO;
}
}
private generateIssueId(): string {
return `issue_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Public utility methods
getSupportedLanguages(): Language[] {
return this.languageDetector.getSupportedLanguages();
}
isLanguageSupported(language: string): boolean {
try {
const parsedLanguage = this.parseLanguage(language);
return this.languageDetector.isLanguageSupported(parsedLanguage);
} catch {
return false;
}
}
async getProjectConfig(projectPath: string, language?: Language): Promise<any> {
try {
const configFiles = await this.configParser.findConfigFiles(projectPath, language);
return this.configParser.mergeConfigs(configFiles);
} catch (error) {
this.logger.error('Error getting project config:', error);
return {};
}
}
// Statistics and reporting
generateAnalysisReport(results: AnalysisResult[]): any {
const totalFiles = results.length;
const totalIssues = results.reduce((sum, result) => sum + result.issues.length, 0);
const issuesBySeverity = {
error: 0,
warning: 0,
info: 0,
hint: 0,
};
const issuesByCategory = {
syntax: 0,
style: 0,
security: 0,
performance: 0,
maintainability: 0,
accessibility: 0,
};
const issuesByLanguage: Record<string, number> = {};
results.forEach(result => {
result.issues.forEach(issue => {
issuesBySeverity[issue.severity as keyof typeof issuesBySeverity]++;
issuesByCategory[issue.category as keyof typeof issuesByCategory]++;
issuesByLanguage[result.language] = (issuesByLanguage[result.language] || 0) + 1;
});
});
return {
summary: {
totalFiles,
totalIssues,
averageIssuesPerFile: totalFiles > 0 ? (totalIssues / totalFiles).toFixed(2) : 0,
},
issuesBySeverity,
issuesByCategory,
issuesByLanguage,
timestamp: new Date().toISOString(),
};
}
}