Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
RustAnalyzer.ts19.4 kB
// Copyright 2025 Chris Bunting // Brief: Rust analyzer for Static Analysis MCP Server // Scope: Provides Clippy and rustfmt 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 RustRule { ruleId: string; description: string; category: IssueCategory; severity: SeverityLevel; tool: 'clippy' | 'rustfmt'; fixable: boolean; } export class RustAnalyzer implements AnalyzerInterface { private logger: Logger; private configParser: ConfigParser; private supportedRules: Map<string, RustRule> = new Map(); private projectConfig: any = null; constructor(logger: Logger, configParser: ConfigParser) { this.logger = logger; this.configParser = configParser; this.initializeRules(); } private initializeRules(): void { // Initialize common Rust rules with metadata const rules: RustRule[] = [ { ruleId: 'dead_code', description: 'Dead code', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'unused_variables', description: 'Unused variable', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'unused_imports', description: 'Unused import', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'needless_return', description: 'Needless return', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'clone_on_copy', description: 'Using clone on type with Copy trait', category: IssueCategory.PERFORMANCE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'eq_op', description: 'Equal operands on both sides of operator', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clippy', fixable: false }, { ruleId: 'if_same_then_else', description: 'If block with same then and else branches', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'formatting', description: 'Code formatting issues', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'rustfmt', fixable: true }, { ruleId: 'complexity', description: 'Code complexity issues', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: false }, { ruleId: 'perf', description: 'Performance issues', category: IssueCategory.PERFORMANCE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'style', description: 'Style issues', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }, { ruleId: 'suspicious', description: 'Suspicious code patterns', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: false } ]; rules.forEach(rule => { this.supportedRules.set(rule.ruleId, rule); }); } async initializeProject(projectPath: string): Promise<void> { try { const configFiles = await this.configParser.findConfigFiles(projectPath, Language.RUST); this.projectConfig = this.configParser.mergeConfigs(configFiles); this.logger.info('Rust project configuration loaded successfully'); } catch (error) { this.projectConfig = {}; this.logger.info('Using default Rust 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 Clippy analysis if (!options.exclude?.includes('clippy')) { const clippyIssues = await this.runClippyAnalysis(filePath, options); issues.push(...clippyIssues); } // Run rustfmt check if (!options.exclude?.includes('rustfmt')) { const rustfmtIssues = await this.runRustfmtAnalysis(filePath, options); issues.push(...rustfmtIssues); } return this.createAnalysisResult(filePath, Language.RUST, issues, metrics); } catch (error) { if (error instanceof AnalysisError) { throw error; } throw ErrorHandler.createAnalysisFailedError(filePath, Language.RUST, error as Error); } } async analyzeProject( projectPath: string, patterns?: string[], excludePatterns?: string[], options: AnalysisOptions = {} ): Promise<AnalysisResult[]> { try { await this.initializeProject(projectPath); const defaultPatterns = [ '**/*.rs', '!**/target/**', '!**/node_modules/**', '!**/.git/**' ]; const filePatterns = patterns || defaultPatterns; const exclude = excludePatterns || []; // Find Rust 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.RUST, 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: ['clippy', 'rustfmt'] }; } catch (error) { throw ErrorHandler.createExecutionError('Rust tools', 'getting rules', error as Error); } } async configure(config: any): Promise<void> { try { if (config.projectPath) { await this.initializeProject(config.projectPath); } this.logger.info('Rust analyzer configured successfully'); } catch (error) { throw ErrorHandler.createInvalidConfigError('rust config', error as Error); } } private async runClippyAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { // Check if this is part of a Cargo project const projectDir = this.findCargoProject(filePath); let command: string; if (projectDir) { // Run clippy on the entire project command = `cd "${projectDir}" && cargo clippy --message-format=json --quiet`; } else { // Run clippy on individual file command = `rustc --crate-type lib --clippy "${filePath}" 2>&1`; } const result = execSync(command, { encoding: 'utf-8', timeout: 60000 }); const issues: AnalysisIssue[] = []; if (projectDir) { // Parse Cargo clippy JSON output const lines = result.split('\n'); for (const line of lines) { if (line.trim()) { try { const clippyMessage = JSON.parse(line); if (clippyMessage.message?.spans) { const issue = this.parseClippyMessage(clippyMessage, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } catch (parseError) { // Ignore JSON parse errors } } } } else { // Parse rustc clippy output const lines = result.split('\n'); for (const line of lines) { if (line.includes('warning') || line.includes('error')) { const issue = this.parseRustcLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } } return issues; } catch (error) { this.logger.warn(`Clippy analysis failed for ${filePath}:`, error); return []; } } private async runRustfmtAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `rustfmt --check "${filePath}"`; const result = execSync(command, { encoding: 'utf-8', timeout: 30000 }); const issues: AnalysisIssue[] = []; // If rustfmt --check produces output, there are formatting issues if (result.length > 0) { const issue: AnalysisIssue = { id: `rustfmt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: 'formatting', message: 'Code formatting issues detected', severity: SeverityLevel.WARNING, line: 1, column: 1, category: IssueCategory.STYLE, fix: { description: 'Format code with rustfmt', replacement: 'Run: rustfmt ' + filePath, range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } }, tags: ['rustfmt', 'formatting'] }; issues.push(issue); } return issues; } catch (error) { this.logger.warn(`Rustfmt analysis failed for ${filePath}:`, error); return []; } } private findCargoProject(filePath: string): string | null { let currentDir = dirname(filePath); while (currentDir !== dirname(currentDir)) { if (existsSync(join(currentDir, 'Cargo.toml'))) { return currentDir; } currentDir = dirname(currentDir); } return null; } private parseClippyMessage(message: any, filePath: string): AnalysisIssue | null { try { if (!message.message?.spans || message.message.spans.length === 0) { return null; } const span = message.message.spans[0]; const rule = this.inferClippyRule(message.message.code?.code || 'clippy.general'); return { id: `clippy_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: message.message.code?.code || 'clippy.general', message: message.message.message, severity: this.mapClippyLevel(message.message.level), line: span.line_start || 1, column: span.column_start || 1, endLine: span.line_end, endColumn: span.column_end, category: rule.category, tags: ['clippy'] }; } catch (error) { this.logger.warn('Failed to parse clippy message:', error); return null; } } private parseRustcLine(line: string, filePath: string): AnalysisIssue | null { try { // Example: file.rs:10:5: warning: unused variable const match = line.match(/^(.+):(\d+):(\d+):\s+(warning|error):\s+(.+)$/); if (!match) { return null; } const [, , lineNum, colNum, level, message] = match; const rule = this.inferClippyRule(message); return { id: `clippy_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: rule.ruleId, message: message.trim(), severity: level === 'error' ? SeverityLevel.ERROR : SeverityLevel.WARNING, line: parseInt(lineNum), column: parseInt(colNum), category: rule.category, tags: ['clippy'] }; } catch (error) { this.logger.warn('Failed to parse rustc line:', error); return null; } } private inferClippyRule(code: string): RustRule { const codeLower = code.toLowerCase(); if (codeLower.includes('dead_code') || codeLower.includes('dead code')) { return { ruleId: 'dead_code', description: 'Dead code', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('unused_var') || codeLower.includes('unused variable')) { return { ruleId: 'unused_variables', description: 'Unused variable', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('unused_import') || codeLower.includes('unused import')) { return { ruleId: 'unused_imports', description: 'Unused import', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('needless_return') || codeLower.includes('needless return')) { return { ruleId: 'needless_return', description: 'Needless return', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('clone_on_copy') || codeLower.includes('clone on copy')) { return { ruleId: 'clone_on_copy', description: 'Using clone on type with Copy trait', category: IssueCategory.PERFORMANCE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('eq_op') || codeLower.includes('equal operands')) { return { ruleId: 'eq_op', description: 'Equal operands on both sides of operator', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'clippy', fixable: false }; } else if (codeLower.includes('if_same_then_else') || codeLower.includes('if same then else')) { return { ruleId: 'if_same_then_else', description: 'If block with same then and else branches', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('complexity')) { return { ruleId: 'complexity', description: 'Code complexity issues', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: false }; } else if (codeLower.includes('perf')) { return { ruleId: 'perf', description: 'Performance issues', category: IssueCategory.PERFORMANCE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('style')) { return { ruleId: 'style', description: 'Style issues', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } else if (codeLower.includes('suspicious')) { return { ruleId: 'suspicious', description: 'Suspicious code patterns', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: false }; } // Default fallback return { ruleId: 'clippy.general', description: 'General clippy warning', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'clippy', fixable: true }; } private mapClippyLevel(level: string): SeverityLevel { switch (level.toLowerCase()) { case 'error': return SeverityLevel.ERROR; case 'warning': return SeverityLevel.WARNING; case 'note': case 'help': return SeverityLevel.INFO; default: return SeverityLevel.INFO; } } 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('///') || 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