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