Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
GoAnalyzer.ts17.9 kB
// Copyright 2025 Chris Bunting // Brief: Go analyzer for Static Analysis MCP Server // Scope: Provides golint, go vet, and staticcheck 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 GoRule { ruleId: string; description: string; category: IssueCategory; severity: SeverityLevel; tool: 'golint' | 'govet' | 'staticcheck'; fixable: boolean; } export class GoAnalyzer implements AnalyzerInterface { private logger: Logger; private configParser: ConfigParser; private supportedRules: Map<string, GoRule> = new Map(); private projectConfig: any = null; constructor(logger: Logger, configParser: ConfigParser) { this.logger = logger; this.configParser = configParser; this.initializeRules(); } private initializeRules(): void { // Initialize common Go rules with metadata const rules: GoRule[] = [ { ruleId: 'exported', description: 'Exported function should have comment or be unexported', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }, { ruleId: 'var-naming', description: 'Don\'t use underscores in Go names', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }, { ruleId: 'package-comment', description: 'Package comment should be of the form', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }, { ruleId: 'deadcode', description: 'Dead code', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }, { ruleId: 'copylocks', description: 'Copy locks with value receiver', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'govet', fixable: false }, { ruleId: 'printf', description: 'Printf format check', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }, { ruleId: 'structtag', description: 'Struct tag format check', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }, { ruleId: 'unreachable', description: 'Unreachable code', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'staticcheck', fixable: true }, { ruleId: 'SA1000', description: 'Invalid regular expression', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'staticcheck', fixable: false }, { ruleId: 'SA2001', description: 'Empty critical section', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'staticcheck', fixable: false }, { ruleId: 'SA4000', description: 'Integer division over integer', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'staticcheck', fixable: true }, { ruleId: 'SA5000', description: 'Invalid build tag', category: IssueCategory.SYNTAX, severity: SeverityLevel.ERROR, tool: 'staticcheck', 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.GO); this.projectConfig = this.configParser.mergeConfigs(configFiles); this.logger.info('Go project configuration loaded successfully'); } catch (error) { this.projectConfig = {}; this.logger.info('Using default Go 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 golint analysis if (!options.exclude?.includes('golint')) { const golintIssues = await this.runGolintAnalysis(filePath, options); issues.push(...golintIssues); } // Run go vet analysis if (!options.exclude?.includes('govet')) { const govetIssues = await this.runGovetAnalysis(filePath, options); issues.push(...govetIssues); } // Run staticcheck analysis if (!options.exclude?.includes('staticcheck')) { const staticcheckIssues = await this.runStaticcheckAnalysis(filePath, options); issues.push(...staticcheckIssues); } return this.createAnalysisResult(filePath, Language.GO, issues, metrics); } catch (error) { if (error instanceof AnalysisError) { throw error; } throw ErrorHandler.createAnalysisFailedError(filePath, Language.GO, error as Error); } } async analyzeProject( projectPath: string, patterns?: string[], excludePatterns?: string[], options: AnalysisOptions = {} ): Promise<AnalysisResult[]> { try { await this.initializeProject(projectPath); const defaultPatterns = [ '**/*.go', '!**/vendor/**', '!**/.git/**', '!**/node_modules/**' ]; const filePatterns = patterns || defaultPatterns; const exclude = excludePatterns || []; // Find Go 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.GO, 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: ['golint', 'govet', 'staticcheck'] }; } catch (error) { throw ErrorHandler.createExecutionError('Go tools', 'getting rules', error as Error); } } async configure(config: any): Promise<void> { try { if (config.projectPath) { await this.initializeProject(config.projectPath); } this.logger.info('Go analyzer configured successfully'); } catch (error) { throw ErrorHandler.createInvalidConfigError('go config', error as Error); } } private async runGolintAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `golint "${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.trim()) { const issue = this.parseGolintLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Golint analysis failed for ${filePath}:`, error); return []; } } private async runGovetAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `go vet "${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.trim() && line.includes(filePath)) { const issue = this.parseGovetLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Go vet analysis failed for ${filePath}:`, error); return []; } } private async runStaticcheckAnalysis(filePath: string, options: AnalysisOptions): Promise<AnalysisIssue[]> { try { const command = `staticcheck "${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.trim() && line.includes(filePath)) { const issue = this.parseStaticcheckLine(line, filePath); if (issue && this.shouldIncludeIssue(issue, options)) { issues.push(issue); } } } return issues; } catch (error) { this.logger.warn(`Staticcheck analysis failed for ${filePath}:`, error); return []; } } private parseGolintLine(line: string, filePath: string): AnalysisIssue | null { try { // Example: /path/to/file.go:10:1: exported function MyFunc should have comment or be unexported const match = line.match(/^(.+):(\d+):(\d+):\s+(.+)$/); if (!match) { return null; } const [, , lineNum, colNum, message] = match; const rule = this.inferGolintRule(message); return { id: `golint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: rule.ruleId, message: message.trim(), severity: rule.severity, line: parseInt(lineNum), column: parseInt(colNum), category: rule.category, tags: ['golint'] }; } catch (error) { this.logger.warn('Failed to parse golint line:', error); return null; } } private parseGovetLine(line: string, filePath: string): AnalysisIssue | null { try { // Example: /path/to/file.go:10:2: missing argument for Printf format const match = line.match(/^(.+):(\d+):(\d+):\s+(.+)$/); if (!match) { return null; } const [, , lineNum, colNum, message] = match; const rule = this.inferGovetRule(message); return { id: `govet_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ruleId: rule.ruleId, message: message.trim(), severity: rule.severity, line: parseInt(lineNum), column: parseInt(colNum), category: rule.category, tags: ['govet'] }; } catch (error) { this.logger.warn('Failed to parse go vet line:', error); return null; } } private parseStaticcheckLine(line: string, filePath: string): AnalysisIssue | null { try { // Example: /path/to/file.go:10:2: SA1000: invalid regular expression const match = line.match(/^(.+):(\d+):(\d+):\s+(SA\d+):\s+(.+)$/); if (!match) { return null; } const [, , lineNum, colNum, ruleId, message] = match; const rule = this.supportedRules.get(ruleId); return { id: `staticcheck_${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.SECURITY, tags: ['staticcheck'] }; } catch (error) { this.logger.warn('Failed to parse staticcheck line:', error); return null; } } private inferGolintRule(message: string): GoRule { const messageLower = message.toLowerCase(); if (messageLower.includes('exported') && messageLower.includes('comment')) { return { ruleId: 'exported', description: 'Exported function should have comment or be unexported', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }; } else if (messageLower.includes('underscore') || messageLower.includes('_')) { return { ruleId: 'var-naming', description: 'Don\'t use underscores in Go names', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }; } else if (messageLower.includes('package comment')) { return { ruleId: 'package-comment', description: 'Package comment should be of the form', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }; } // Default fallback return { ruleId: 'golint.general', description: 'General golint warning', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'golint', fixable: true }; } private inferGovetRule(message: string): GoRule { const messageLower = message.toLowerCase(); if (messageLower.includes('dead code')) { return { ruleId: 'deadcode', description: 'Dead code', category: IssueCategory.MAINTAINABILITY, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }; } else if (messageLower.includes('copy') && messageLower.includes('lock')) { return { ruleId: 'copylocks', description: 'Copy locks with value receiver', category: IssueCategory.SECURITY, severity: SeverityLevel.ERROR, tool: 'govet', fixable: false }; } else if (messageLower.includes('printf') || messageLower.includes('format')) { return { ruleId: 'printf', description: 'Printf format check', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }; } else if (messageLower.includes('struct') && messageLower.includes('tag')) { return { ruleId: 'structtag', description: 'Struct tag format check', category: IssueCategory.STYLE, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }; } // Default fallback return { ruleId: 'govet.general', description: 'General go vet warning', category: IssueCategory.SECURITY, severity: SeverityLevel.WARNING, tool: 'govet', fixable: true }; } 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('*') ).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