Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
CppAnalyzer.ts16.8 kB
// Copyright 2025 Chris Bunting // Brief: C/C++ analyzer for Static Analysis MCP Server // Scope: Provides Clang Static Analyzer and cppcheck integration import { execSync } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { join, dirname, extname, basename } from 'path'; import { AnalysisResult, AnalysisIssue, AnalysisMetrics, AnalysisOptions, SeverityLevel, IssueCategory, Language } 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 } from '../utils/ErrorHandler.js'; export interface CppRule { ruleId: string; description: string; category: IssueCategory; severity: SeverityLevel; tool: 'clang' | 'cppcheck'; fixable: boolean; } export class CppAnalyzer implements AnalyzerInterface { private logger: Logger; private configParser: ConfigParser; private supportedRules: Map<string, CppRule> = new Map(); private _projectConfig: any = null; constructor(logger: Logger, configParser: ConfigParser) { this.logger = logger; this.configParser = configParser; this.initializeRules(); } private initializeRules(): void { // Initialize common C/C++ rules with metadata const rules: CppRule[] = [ { ruleId: 'core.NullDereference', description: 'Null pointer dereference', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clang', fixable: false }, { ruleId: 'core.DivideZero', description: 'Division by zero', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clang', fixable: false }, { ruleId: 'core.StackAddressEscape', description: 'Stack address escape', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clang', fixable: false }, { ruleId: 'cplusplus.NewDelete', description: 'Mismatched new/delete', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clang', fixable: true }, { ruleId: 'unix.Malloc', description: 'Potential memory leak', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clang', fixable: true }, { ruleId: 'nullPointer', description: 'Null pointer dereference', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'cppcheck', fixable: false }, { ruleId: 'arrayIndexOutOfBounds', description: 'Array index out of bounds', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'cppcheck', fixable: false }, { ruleId: 'uninitvar', description: 'Uninitialized variable', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'cppcheck', fixable: true }, { ruleId: 'memoryLeak', description: 'Memory leak', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'cppcheck', fixable: true }, { ruleId: 'unusedVariable', description: 'Unused variable', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'cppcheck', 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.C); this._projectConfig = this.configParser.mergeConfigs(configFiles); this.logger.info('C/C++ project configuration loaded successfully'); } catch (error) { this._projectConfig = {}; this.logger.info('Using default C/C++ 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); const language = this.detectLanguage(filePath); // Run Clang Static Analyzer analysis if (!options.exclude?.includes('clang')) { const clangIssues = await this.runClangAnalysis(filePath, options); issues.push(...clangIssues); } // Run cppcheck analysis if (!options.exclude?.includes('cppcheck')) { const cppcheckIssues = await this.runCppcheckAnalysis(filePath, options); issues.push(...cppcheckIssues); } return this.createAnalysisResult(filePath, language, issues, metrics); } catch (error) { if (error instanceof AnalysisError) { throw error; } const language = this.detectLanguage(filePath); throw ErrorHandler.createAnalysisFailedError(filePath, language, error as Error); } } async analyzeProject( projectPath: string, patterns?: string[], excludePatterns?: string[], options: AnalysisOptions = {} ): Promise<AnalysisResult[]> { try { await this.initializeProject(projectPath); const defaultPatterns = [ '**/*.{c,cpp,cxx,cc,h,hpp,hxx}', '!**/build/**', '!**/dist/**', '!**/target/**', '!**/node_modules/**', '!**/.git/**', '!**/CMakeFiles/**' ]; const filePatterns = patterns || defaultPatterns; const exclude = excludePatterns || []; // Find C/C++ 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, 'c/c++', 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: ['clang', 'cppcheck'] }; } catch (error) { throw ErrorHandler.createExecutionError('C/C++ tools', 'getting rules', error as Error); } } async configure(config: any): Promise<void> { try { if (config.projectPath) { await this.initializeProject(config.projectPath); } this.logger.info('C/C++ analyzer configured successfully'); } catch (error) { throw ErrorHandler.createInvalidConfigError('c/c++ config', error as Error); } } private async runClangAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { // First compile the file to get AST const compiledPath = await this.compileCppFile(filePath); if (!compiledPath) { return []; } // Run clang static analyzer const command = `scan-build --status-bugs --html-title="Clang Analysis" clang++ -c "${filePath}"`; const result = execSync(command, { encoding: 'utf-8', timeout: 60000 }); const issues: AnalysisIssue[] = []; // Parse clang analyzer output const lines = result.split('\n'); for (const line of lines) { if (line.includes(filePath) && (line.includes('warning:') || line.includes('error:'))) { const issue = this.parseClangLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Clang analysis failed for ${filePath}:`, error); return []; } } private async runCppcheckAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `cppcheck --enable=all --xml --xml-version=2 "${filePath}"`; const result = execSync(command, { encoding: 'utf-8', timeout: 30000 }); const issues: AnalysisIssue[] = []; // Parse cppcheck XML output if (result.includes('<error')) { const errors = result.match(/<error[^>]*>[\s\S]*?<\/error>/g) || []; for (const error of errors) { const issue = this.parseCppcheckError(error, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Cppcheck analysis failed for ${filePath}:`, error); return []; } } private async compileCppFile(filePath: string): Promise<string | null> { try { const dir = dirname(filePath); const fileName = basename(filePath, extname(filePath)); const objFile = join(dir, `${fileName}.o`); // Check if already compiled if (existsSync(objFile)) { return objFile; } // Compile the file const language = this.detectLanguage(filePath); const compiler = language === Language.CPP ? 'g++' : 'gcc'; const command = `${compiler} -c "${filePath}" -o "${objFile}"`; execSync(command, { encoding: 'utf-8', timeout: 30000 }); return existsSync(objFile) ? objFile : null; } catch (error) { this.logger.warn(`Failed to compile C/C++ file ${filePath}:`, error); return null; } } private parseClangLine(line: string, _filePath: string): AnalysisIssue | null { try { // Example: /path/to/file.cpp:10:5: warning: Null pointer dereference const match = line.match(/^(.+):(\d+):(\d+):\s+(warning|error):\s+(.+)$/); if (!match) { return null; } const [, , lineNum, colNum, severity, message] = match; const rule = this.inferClangRule(message); return { id: `clang_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: rule.ruleId, message: message.trim(), severity: severity === 'error' ? SeverityLevel.ERROR : SeverityLevel.WARNING, line: parseInt(lineNum), column: parseInt(colNum), category: rule.category, tags: ['clang'] }; } catch (error) { this.logger.warn('Failed to parse Clang line:', error); return null; } } private parseCppcheckError(error: string, _filePath: string): AnalysisIssue | null { try { const lineMatch = error.match(/line="(\d+)"/); const idMatch = error.match(/id="([^"]+)"/); const severityMatch = error.match(/severity="([^"]+)"/); const messageMatch = error.match(/<msg>([^<]+)<\/msg>/); if (!lineMatch || !idMatch || !severityMatch || !messageMatch) { return null; } const rule = this.supportedRules.get(idMatch[1]); return { id: `cppcheck_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: idMatch[1], message: messageMatch[1].trim(), severity: this.mapCppcheckSeverity(severityMatch[1]), line: parseInt(lineMatch[1]), column: 1, category: rule?.category || IssueCategory.SECURITY, tags: ['cppcheck'] }; } catch (error) { this.logger.warn('Failed to parse Cppcheck error:', error); return null; } } private inferClangRule(message: string): CppRule { const messageLower = message.toLowerCase(); if (messageLower.includes('null') || messageLower.includes('nullptr')) { return { ruleId: 'core.NullDereference', description: 'Null pointer dereference', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clang', fixable: false }; } else if (messageLower.includes('division by zero') || messageLower.includes('divide by zero')) { return { ruleId: 'core.DivideZero', description: 'Division by zero', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clang', fixable: false }; } else if (messageLower.includes('new') && messageLower.includes('delete')) { return { ruleId: 'cplusplus.NewDelete', description: 'Mismatched new/delete', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clang', fixable: true }; } else if (messageLower.includes('malloc') || messageLower.includes('free')) { return { ruleId: 'unix.Malloc', description: 'Potential memory leak', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clang', fixable: true }; } // Default fallback return { ruleId: 'clang.general', description: 'General clang warning', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clang', fixable: false }; } private mapCppcheckSeverity(severity: string): SeverityLevel { switch (severity.toLowerCase()) { case 'error': return SeverityLevel.ERROR; case 'warning': return SeverityLevel.WARNING; case 'style': case 'performance': case 'portability': return SeverityLevel.INFO; default: return SeverityLevel.INFO; } } private detectLanguage(filePath: string): Language { const extension = extname(filePath).toLowerCase(); switch (extension) { case '.c': case '.h': return Language.C; case '.cpp': case '.cxx': case '.cc': case '.hpp': case '.hxx': return Language.CPP; default: return Language.C; // Default fallback } } 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