Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
JavaAnalyzer.ts15.9 kB
// Copyright 2025 Chris Bunting // Brief: Java analyzer for Static Analysis MCP Server // Scope: Provides SpotBugs, PMD, and Checkstyle integration import { execSync, exec } from 'child_process'; import { readFileSync, existsSync, writeFileSync } from 'fs'; import { join, dirname, extname, basename } from 'path'; import { AnalysisResult, AnalysisIssue, AnalysisMetrics, AnalysisOptions, SeverityLevel, IssueCategory, Language, FixSuggestion } from '@mcp-code-analysis/shared-types'; import { AnalyzerInterface } from '../services/StaticAnalysisService.js'; import { Logger } from '../utils/Logger.js'; import { ConfigParser } from '../services/ConfigParser.js'; import { ErrorHandler, AnalysisError, ErrorCode } from '../utils/ErrorHandler.js'; export interface JavaRule { ruleId: string; description: string; category: IssueCategory; severity: SeverityLevel; tool: 'spotbugs' | 'pmd' | 'checkstyle'; fixable: boolean; } export class JavaAnalyzer implements AnalyzerInterface { private logger: Logger; private configParser: ConfigParser; private supportedRules: Map<string, JavaRule> = new Map(); private projectConfig: any = null; constructor(logger: Logger, configParser: ConfigParser) { this.logger = logger; this.configParser = configParser; this.initializeRules(); } private initializeRules(): void { // Initialize common Java rules with metadata const rules: JavaRule[] = [ { ruleId: 'UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR', description: 'Field not initialized in constructor', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'spotbugs', fixable: true }, { ruleId: 'NP_NULL_ON_SOME_PATH', description: 'Possible null pointer dereference', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'spotbugs', fixable: false }, { ruleId: 'DM_DEFAULT_ENCODING', description: 'Reliance on default encoding', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'spotbugs', fixable: true }, { ruleId: 'EmptyStatement', description: 'Empty statement', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'pmd', fixable: true }, { ruleId: 'UnusedLocalVariable', description: 'Unused local variable', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'pmd', fixable: true }, { ruleId: 'EmptyCatchBlock', description: 'Empty catch block', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'pmd', fixable: true }, { ruleId: 'MissingSwitchDefault', description: 'Missing default in switch', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'pmd', fixable: true }, { ruleId: 'MethodLength', description: 'Method length', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'checkstyle', fixable: false }, { ruleId: 'LineLength', description: 'Line length', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'checkstyle', fixable: true } ]; rules.forEach(rule => { this.supportedRules.set(rule.ruleId, rule); }); } async initializeProject(projectPath: string): Promise<void> { try { const configFiles = await this.configParser.findConfigFiles(projectPath, Language.JAVA); this.projectConfig = this.configParser.mergeConfigs(configFiles); this.logger.info('Java project configuration loaded successfully'); } catch (error) { this.projectConfig = {}; this.logger.info('Using default Java configuration'); } } async analyzeFile(filePath: string, options: AnalysisOptions = {}): Promise<AnalysisResult> { try { if (!existsSync(filePath)) { throw ErrorHandler.createFileNotFoundError(filePath); } const content = readFileSync(filePath, 'utf-8'); const issues: AnalysisIssue[] = []; const metrics = this.calculateMetrics(content); // Run SpotBugs analysis if (!options.exclude?.includes('spotbugs')) { const spotbugsIssues = await this.runSpotBugsAnalysis(filePath, options); issues.push(...spotbugsIssues); } // Run PMD analysis if (!options.exclude?.includes('pmd')) { const pmdIssues = await this.runPMDAnalysis(filePath, options); issues.push(...pmdIssues); } // Run Checkstyle analysis if (!options.exclude?.includes('checkstyle')) { const checkstyleIssues = await this.runCheckstyleAnalysis(filePath, options); issues.push(...checkstyleIssues); } return this.createAnalysisResult(filePath, Language.JAVA, issues, metrics); } catch (error) { if (error instanceof AnalysisError) { throw error; } throw ErrorHandler.createAnalysisFailedError(filePath, Language.JAVA, error as Error); } } async analyzeProject( projectPath: string, patterns?: string[], excludePatterns?: string[], options: AnalysisOptions = {} ): Promise<AnalysisResult[]> { try { await this.initializeProject(projectPath); const defaultPatterns = [ '**/*.java', '!**/target/**', '!**/build/**', '!**/dist/**', '!**/node_modules/**', '!**/.git/**' ]; const filePatterns = patterns || defaultPatterns; const exclude = excludePatterns || []; // Find Java files const { glob } = await import('fast-glob'); const files = await glob(filePatterns, { cwd: projectPath, ignore: exclude, absolute: true, onlyFiles: true }); const results: AnalysisResult[] = []; for (const file of files) { try { const result = await this.analyzeFile(file, options); results.push(result); } catch (error) { this.logger.warn(`Failed to analyze file ${file}:`, error); } } return results; } catch (error) { throw ErrorHandler.createAnalysisFailedError(projectPath, Language.JAVA, error as Error); } } async getRules(configFile?: string): Promise<any> { try { const allRules: any[] = []; // Add our predefined rules Array.from(this.supportedRules.values()).forEach(rule => { allRules.push({ ...rule, enabled: true // All rules are enabled by default }); }); return { rules: allRules, total: allRules.length, enabled: allRules.filter(r => r.enabled).length, tools: ['spotbugs', 'pmd', 'checkstyle'] }; } catch (error) { throw ErrorHandler.createExecutionError('Java tools', 'getting rules', error as Error); } } async configure(config: any): Promise<void> { try { if (config.projectPath) { await this.initializeProject(config.projectPath); } this.logger.info('Java analyzer configured successfully'); } catch (error) { throw ErrorHandler.createInvalidConfigError('java config', error as Error); } } private async runSpotBugsAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { // First compile the Java file if needed const compiledPath = await this.compileJavaFile(filePath); if (!compiledPath) { return []; } const command = `spotbugs -textui -xml ${compiledPath}`; const result = execSync(command, { encoding: 'utf-8', timeout: 60000 }); const issues: AnalysisIssue[] = []; // Parse SpotBugs XML output if (result.includes('<BugInstance')) { const bugInstances = result.match(/<BugInstance[^>]*>[\s\S]*?<\/BugInstance>/g) || []; for (const bugInstance of bugInstances) { const issue = this.parseSpotBugsBugInstance(bugInstance, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`SpotBugs analysis failed for ${filePath}:`, error); return []; } } private async runPMDAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `pmd -d ${filePath} -f xml -R rulesets/java/quickstart.xml`; const result = execSync(command, { encoding: 'utf-8', timeout: 30000 }); const issues: AnalysisIssue[] = []; // Parse PMD XML output if (result.includes('<violation')) { const violations = result.match(/<violation[^>]*>[\s\S]*?<\/violation>/g) || []; for (const violation of violations) { const issue = this.parsePMDViolation(violation, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`PMD analysis failed for ${filePath}:`, error); return []; } } private async runCheckstyleAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `checkstyle -c /sun_checks.xml ${filePath}`; const result = execSync(command, { encoding: 'utf-8', timeout: 30000 }); const issues: AnalysisIssue[] = []; const lines = result.split('\n'); for (const line of lines) { if (line.includes('ERROR') || line.includes('WARNING')) { const issue = this.parseCheckstyleLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Checkstyle analysis failed for ${filePath}:`, error); return []; } } private async compileJavaFile(filePath: string): Promise<string | null> { try { const dir = dirname(filePath); const fileName = basename(filePath, '.java'); const classFile = join(dir, `${fileName}.class`); // Check if already compiled if (existsSync(classFile)) { return classFile; } // Compile the file const command = `javac "${filePath}"`; execSync(command, { encoding: 'utf-8', timeout: 30000 }); return existsSync(classFile) ? classFile : null; } catch (error) { this.logger.warn(`Failed to compile Java file ${filePath}:`, error); return null; } } private parseSpotBugsBugInstance(bugInstance: string, filePath: string): AnalysisIssue | null { try { const typeMatch = bugInstance.match(/type="([^"]+)"/); const messageMatch = bugInstance.match(/<ShortMessage>([^<]+)<\/ShortMessage>/); const lineMatch = bugInstance.match(/<SourceLine[^>]*start="(\d+)"/); if (!typeMatch || !messageMatch) { return null; } const rule = this.supportedRules.get(typeMatch[1]); return { id: `spotbugs_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: typeMatch[1], message: messageMatch[1].trim(), severity: rule?.severity || SeverityLevel.WARNING, line: lineMatch ? parseInt(lineMatch[1]) : 1, column: 1, category: rule?.category || IssueCategory.MAINTAINABILITY, tags: ['spotbugs'] }; } catch (error) { this.logger.warn('Failed to parse SpotBugs bug instance:', error); return null; } } private parsePMDViolation(violation: string, filePath: string): AnalysisIssue | null { try { const lineMatch = violation.match(/beginline="(\d+)"/); const ruleMatch = violation.match(/rule="([^"]+)"/); const messageMatch = violation.match(/>([^<]+)</); if (!lineMatch || !ruleMatch || !messageMatch) { return null; } const rule = this.supportedRules.get(ruleMatch[1]); return { id: `pmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: ruleMatch[1], message: messageMatch[1].trim(), severity: rule?.severity || SeverityLevel.WARNING, line: parseInt(lineMatch[1]), column: 1, category: rule?.category || IssueCategory.MAINTAINABILITY, tags: ['pmd'] }; } catch (error) { this.logger.warn('Failed to parse PMD violation:', error); return null; } } private parseCheckstyleLine(line: string, filePath: string): AnalysisIssue | null { try { const match = line.match(/ERROR\s+(\d+):(\d+)\s+-\s+([^:]+):\s+(.+)$/); if (!match) { return null; } const [, lineNum, colNum, ruleId, message] = match; const rule = this.supportedRules.get(ruleId); return { id: `checkstyle_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId, message: message.trim(), severity: rule?.severity || SeverityLevel.WARNING, line: parseInt(lineNum), column: parseInt(colNum), category: rule?.category || IssueCategory.STYLE, tags: ['checkstyle'] }; } catch (error) { this.logger.warn('Failed to parse Checkstyle line:', error); return null; } } private shouldIncludeIssue(issue: AnalysisIssue, options: AnalysisOptions): boolean { // Filter by rules if (options.rules && options.rules.length > 0) { if (!options.rules.includes(issue.ruleId)) { return false; } } // Filter by excluded rules if (options.exclude && options.exclude.length > 0) { if (options.exclude.includes(issue.ruleId)) { return false; } } // Filter by fixable if (options.fixable && !issue.fix) { return false; } return true; } private calculateMetrics(content: string): AnalysisMetrics { const lines = content.split('\n'); const linesOfCode = lines.length; const commentLines = lines.filter(line => line.trim().startsWith('//') || line.trim().startsWith('/*') || line.trim().startsWith('*') || line.trim().startsWith('*') ).length; return { linesOfCode, commentLines, cyclomaticComplexity: 0, // Would need more sophisticated analysis cognitiveComplexity: 0, // Would need more sophisticated analysis maintainabilityIndex: 0 // Would need more sophisticated analysis }; } private createAnalysisResult( filePath: string, language: Language, issues: AnalysisIssue[], metrics: AnalysisMetrics ): AnalysisResult { return { id: `analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), filePath, language, issues, metrics, severity: this.calculateOverallSeverity(issues) }; } 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; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbunting99/mcp-code-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server