// 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;
}
}
}