code-auditor-mcp
Version:
Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more
611 lines • 26.8 kB
JavaScript
/**
* Go Language Adapter
*
* Provides Go language support through the LanguageAdapter interface.
* Uses the Go-based analyzer as a child process for proper AST analysis.
*/
import path from 'path';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { IS_DEV_MODE } from '../constants.js';
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class GoAdapter {
name = 'go';
extensions = ['.go'];
goAnalyzerProcess = null;
requestId = 0;
pendingRequests = new Map();
goExecutablePath;
logFile;
isInitialized = false;
initializationPromise = null;
jsonBuffer = '';
constructor(goExecutablePath = 'go') {
this.goExecutablePath = goExecutablePath;
// Create log file for Go child process debugging (only in dev mode)
if (IS_DEV_MODE) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
this.logFile = path.join(process.cwd(), `go-analyzer-${timestamp}.log`);
console.error(`[DEBUG] Go: Development mode - logs will be written to: ${this.logFile}`);
// Initialize log file
this.writeLog('=== Go Analyzer Process Log Started ===');
this.writeLog(`Go executable path: ${this.goExecutablePath}`);
this.writeLog(`Path is absolute: ${path.isAbsolute(this.goExecutablePath)}`);
}
else {
this.logFile = ''; // No logging in production
}
console.error(`[DEBUG] Go: Initialized with executable path: ${this.goExecutablePath}`);
// Register cleanup on process exit
this.registerProcessCleanup();
}
writeLog(message) {
// Only log to file in development mode
if (!IS_DEV_MODE || !this.logFile)
return;
try {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
fs.appendFileSync(this.logFile, logEntry);
}
catch (error) {
console.error('[DEBUG] Go: Failed to write to log file:', error);
}
}
registerProcessCleanup() {
// Cleanup on various exit signals
const cleanup = () => {
console.error('[DEBUG] Go: Process cleanup triggered');
this.writeLog('Process cleanup triggered by exit signal');
this.cleanup();
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
process.on('beforeExit', cleanup);
this.writeLog('Registered process cleanup handlers');
}
async parse(file, content) {
console.error(`[DEBUG] Go: Parsing file using persistent child process: ${file} (content length: ${content.length})`);
this.writeLog(`parse() called for ${file}, content length: ${content.length}`);
try {
// Ensure the process is initialized (lazy initialization)
await this.ensureInitialized();
// Use persistent process to analyze the file
const result = await this.analyzeFileWithPersistentProcess(file, content);
console.error(`[DEBUG] Go: Child process returned result with ${result.Violations?.length || 0} violations and ${result.IndexEntries?.length || 0} entities`);
this.writeLog(`Child process returned ${result.Violations?.length || 0} violations and ${result.IndexEntries?.length || 0} entities`);
// Convert Go analysis result to our AST format
const ast = this.convertToAST(file, content, result);
console.error(`[DEBUG] Go: Successfully parsed ${file} - AST conversion complete`);
this.writeLog(`Successfully parsed ${file} - AST conversion complete`);
return ast;
}
catch (error) {
console.error(`[DEBUG] Go: Error parsing ${file}:`, error);
console.error(`[DEBUG] Go: Error stack:`, error.stack);
// Return a minimal AST with error information
const lines = content.split('\n');
return {
root: {
type: 'SourceFile',
range: [0, content.length],
location: {
start: { line: 1, column: 1 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 1 }
},
raw: { error: error.message }
},
language: this.name,
filePath: file,
errors: [{
message: error.message,
location: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
severity: 'error'
}]
};
}
}
async ensureInitialized() {
if (this.isInitialized && this.goAnalyzerProcess) {
return;
}
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = this.initializeProcess();
return this.initializationPromise;
}
async initializeProcess() {
try {
await this.ensureGoAnalyzer();
this.isInitialized = true;
this.writeLog('Persistent Go analyzer process initialized successfully');
}
catch (error) {
this.initializationPromise = null;
throw error;
}
}
async analyzeFileWithPersistentProcess(file, content) {
if (!this.goAnalyzerProcess) {
throw new Error('Go analyzer process not available');
}
const options = {
Analyzers: ['solid', 'imports'],
MinSeverity: 'info',
IncludeIndexing: true
};
const request = {
method: 'analyzeContent',
params: {
file: file,
content: content,
options: options
},
id: ++this.requestId
};
this.writeLog(`Sending analysis request for ${file} (using content instead of file path)`);
return new Promise((resolve, reject) => {
// Set timeout for request
const timeout = setTimeout(() => {
this.pendingRequests.delete(request.id);
reject(new Error(`Go analyzer request timeout after 10 seconds`));
}, 10000);
this.pendingRequests.set(request.id, {
resolve: (result) => {
clearTimeout(timeout);
resolve(result);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
}
});
const requestLine = JSON.stringify(request) + '\n';
this.writeLog(`Sending JSON-RPC request: ${requestLine.trim()}`);
try {
this.goAnalyzerProcess.stdin?.write(requestLine);
this.writeLog(`Request sent successfully to stdin`);
}
catch (error) {
this.writeLog(`Error writing to stdin: ${error.message}`);
reject(error);
}
});
}
async analyzeFile(file) {
console.error(`[DEBUG] Go: analyzeFile called for: ${file}`);
try {
await this.ensureGoAnalyzer();
console.error(`[DEBUG] Go: Go analyzer ensured successfully`);
}
catch (error) {
console.error(`[DEBUG] Go: Failed to ensure analyzer:`, error);
throw error;
}
const options = {
Analyzers: ['solid', 'imports'],
MinSeverity: 'info',
IncludeIndexing: true
};
const request = {
method: 'analyze',
params: {
files: [file],
options: options
},
id: ++this.requestId
};
console.error(`[DEBUG] Go: Created request with ID ${request.id}`);
return new Promise((resolve, reject) => {
// Set timeout for request
const timeout = setTimeout(() => {
this.pendingRequests.delete(request.id);
reject(new Error(`Go analyzer request timeout after 10 seconds`));
}, 10000);
this.pendingRequests.set(request.id, {
resolve: (result) => {
clearTimeout(timeout);
resolve(result);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
}
});
if (!this.goAnalyzerProcess) {
reject(new Error('Go analyzer process not available after ensure'));
return;
}
// Send the request
const requestLine = JSON.stringify(request) + '\n';
console.error(`[DEBUG] Go: Sending request to analyzer: ${requestLine.trim()}`);
this.writeLog(`Sending JSON-RPC request: ${requestLine.trim()}`);
try {
this.goAnalyzerProcess.stdin?.write(requestLine);
console.error(`[DEBUG] Go: Request sent successfully`);
this.writeLog(`Request sent successfully to stdin`);
}
catch (error) {
console.error(`[DEBUG] Go: Error writing to stdin:`, error);
this.writeLog(`Error writing to stdin: ${error.message}`);
reject(error);
}
});
}
async ensureGoAnalyzer() {
if (this.goAnalyzerProcess) {
console.error('[DEBUG] Go: Analyzer process already running');
return;
}
console.error('[DEBUG] Go: Starting Go analyzer process...');
// Path to the Go analyzer binary
// __dirname points to dist/adapters, so we need to go to src/languages/go
const goAnalyzerPath = path.join(__dirname, '../../src/languages/go/main.go');
const goModPath = path.dirname(goAnalyzerPath);
console.error(`[DEBUG] Go: Go analyzer path: ${goAnalyzerPath}`);
console.error(`[DEBUG] Go: Go mod path: ${goModPath}`);
try {
// Start the Go analyzer as a child process
console.error(`[DEBUG] Go: Spawning ${this.goExecutablePath} run main.go...`);
this.writeLog(`Starting Go analyzer: ${this.goExecutablePath} run main.go`);
this.writeLog(`Working directory: ${goModPath}`);
this.goAnalyzerProcess = spawn(this.goExecutablePath, ['run', 'main.go'], {
cwd: goModPath,
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env // Inherit full environment including PATH
});
console.error(`[DEBUG] Go: Process spawned with PID: ${this.goAnalyzerProcess.pid}`);
this.writeLog(`Process spawned with PID: ${this.goAnalyzerProcess.pid}`);
// Handle stdout (JSON-RPC responses) with proper buffering for large responses
this.goAnalyzerProcess.stdout?.on('data', (data) => {
const dataStr = data.toString();
console.error(`[DEBUG] Go: Raw stdout chunk (${dataStr.length} chars): ${dataStr.substring(0, 200)}...`);
this.writeLog(`STDOUT Chunk (${dataStr.length} chars): ${dataStr.substring(0, 200)}${dataStr.length > 200 ? '...[TRUNCATED]' : ''}`);
// Add to JSON buffer
this.jsonBuffer += dataStr;
// Process complete JSON objects from buffer
this.processJsonBuffer();
});
// Handle stderr (debug output)
this.goAnalyzerProcess.stderr?.on('data', (data) => {
const stderrStr = data.toString().trim();
console.error(`[GoAnalyzer] ${stderrStr}`);
this.writeLog(`STDERR: ${stderrStr}`);
});
// Handle process exit
this.goAnalyzerProcess.on('exit', (code) => {
console.error(`[DEBUG] Go: Analyzer process exited with code ${code}`);
this.writeLog(`Process exited with code: ${code}`);
this.goAnalyzerProcess = null;
// Reject all pending requests
for (const [id, pending] of this.pendingRequests) {
pending.reject(new Error(`Go analyzer process exited with code ${code}`));
}
this.pendingRequests.clear();
});
// Handle process error
this.goAnalyzerProcess.on('error', (error) => {
console.error(`[DEBUG] Go: Process error:`, error);
this.writeLog(`Process error: ${JSON.stringify(error)}`);
if (error.code === 'ENOENT') {
console.error(`[DEBUG] Go: 'go' command not found. Is Go installed and in your system's PATH?`);
this.writeLog(`ENOENT: Go command not found`);
}
this.goAnalyzerProcess = null;
// Reject all pending requests
for (const [id, pending] of this.pendingRequests) {
pending.reject(new Error(`Go command not found: ${error.message}`));
}
this.pendingRequests.clear();
});
// Wait a bit for the process to start
await new Promise(resolve => setTimeout(resolve, 1000));
// Test the connection
console.error('[DEBUG] Go: Testing connection with ping...');
const pingResult = await this.sendPing();
console.error('[DEBUG] Go: Analyzer ready, ping result:', pingResult);
}
catch (error) {
console.error('[DEBUG] Go: Failed to start analyzer process:', error);
console.error('[DEBUG] Go: Error stack:', error.stack);
throw new Error(`Failed to start Go analyzer: ${error.message}`);
}
}
async sendPing() {
const request = {
method: 'ping',
params: {},
id: ++this.requestId
};
return new Promise((resolve, reject) => {
this.pendingRequests.set(request.id, { resolve, reject });
if (!this.goAnalyzerProcess) {
this.writeLog('Ping failed: Go analyzer process not available');
reject(new Error('Go analyzer process not available'));
return;
}
const requestLine = JSON.stringify(request) + '\n';
this.writeLog(`Sending ping request: ${requestLine.trim()}`);
this.goAnalyzerProcess.stdin?.write(requestLine);
});
}
convertToAST(file, content, result) {
console.error(`[DEBUG] Go: convertToAST called for: ${file}`);
this.writeLog(`convertToAST() called for ${file}`);
const lines = content.split('\n');
// Log the raw result we got from Go analyzer
console.error(`[DEBUG] Go: Raw result from Go analyzer:`, {
violations: result.Violations?.length || 0,
indexEntries: result.IndexEntries?.length || 0,
errors: result.Errors?.length || 0
});
this.writeLog(`Raw result: ${result.Violations?.length || 0} violations, ${result.IndexEntries?.length || 0} index entries, ${result.Errors?.length || 0} errors`);
if (result.IndexEntries) {
const functionEntries = result.IndexEntries.filter(e => e.Type === 'function');
const structEntries = result.IndexEntries.filter(e => e.Type === 'struct');
const interfaceEntries = result.IndexEntries.filter(e => e.Type === 'interface');
console.error(`[DEBUG] Go: Filtering IndexEntries - functions: ${functionEntries.length}, structs: ${structEntries.length}, interfaces: ${interfaceEntries.length}`);
this.writeLog(`Filtering IndexEntries - functions: ${functionEntries.length}, structs: ${structEntries.length}, interfaces: ${interfaceEntries.length}`);
if (functionEntries.length > 0) {
console.error(`[DEBUG] Go: Sample function entry:`, functionEntries[0]);
this.writeLog(`Sample function entry: ${JSON.stringify(functionEntries[0])}`);
}
}
// Convert Go analysis result to our unified AST format
const entities = {
functions: result.IndexEntries?.filter(e => e.Type === 'function') || [],
structs: result.IndexEntries?.filter(e => e.Type === 'struct') || [],
interfaces: result.IndexEntries?.filter(e => e.Type === 'interface') || [],
violations: result.Violations || []
};
console.error(`[DEBUG] Go: Final entities object:`, {
functions: entities.functions.length,
structs: entities.structs.length,
interfaces: entities.interfaces.length,
violations: entities.violations.length
});
this.writeLog(`Final entities: ${entities.functions.length} functions, ${entities.structs.length} structs, ${entities.interfaces.length} interfaces, ${entities.violations.length} violations`);
return {
root: {
type: 'SourceFile',
range: [0, content.length],
location: {
start: { line: 1, column: 1 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 1 }
},
raw: entities
},
language: this.name,
filePath: file,
errors: result.Errors?.map(err => ({
message: err.Message,
location: {
start: { line: err.Line, column: 1 },
end: { line: err.Line, column: 1 }
},
severity: 'error'
})) || []
};
}
supportsFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
return this.extensions.includes(ext);
}
findNodes(ast, pattern) {
const results = [];
const entities = ast.root.raw;
if (pattern.type === 'function' && entities.functions) {
for (const func of entities.functions) {
results.push(this.createFunctionNode(func));
}
}
if (pattern.type === 'struct' && entities.structs) {
for (const struct of entities.structs) {
results.push(this.createStructNode(struct));
}
}
return results;
}
createFunctionNode(func) {
return {
type: 'function',
range: [0, 0], // Simplified
location: {
start: { line: func.StartLine, column: 1 },
end: { line: func.EndLine, column: 1 }
},
raw: func
};
}
createStructNode(struct) {
return {
type: 'struct',
range: [0, 0], // Simplified
location: {
start: { line: struct.StartLine, column: 1 },
end: { line: struct.EndLine, column: 1 }
},
raw: struct
};
}
extractFunctions(ast) {
console.error(`[DEBUG] Go: extractFunctions called for: ${ast.filePath}`);
this.writeLog(`extractFunctions() called for ${ast.filePath}`);
const entities = ast.root.raw;
console.error(`[DEBUG] Go: Raw entities:`, JSON.stringify(entities, null, 2).substring(0, 500) + '...');
this.writeLog(`Raw entities keys: ${Object.keys(entities || {})}`);
if (!entities.functions) {
console.error(`[DEBUG] Go: No functions found in entities for ${ast.filePath}`);
this.writeLog(`No functions found in entities for ${ast.filePath}`);
return [];
}
console.error(`[DEBUG] Go: Found ${entities.functions.length} functions in ${ast.filePath}`);
this.writeLog(`Found ${entities.functions.length} functions in ${ast.filePath}`);
return entities.functions.map((func) => ({
name: func.Name,
location: {
start: { line: func.StartLine, column: 1 },
end: { line: func.EndLine, column: 1 }
},
parameters: (func.Parameters || []).map((param) => ({
name: param,
type: 'unknown', // Could be enhanced
optional: false,
defaultValue: undefined
})),
returnType: func.ReturnType || undefined,
isAsync: false, // Go doesn't have async/await
isExported: func.IsExported,
isMethod: func.Type === 'method',
className: undefined, // Go uses receivers, not classes
complexity: func.Complexity || 1,
lineCount: func.EndLine - func.StartLine + 1
}));
}
extractClasses(ast) {
const entities = ast.root.raw;
if (!entities.structs)
return [];
// In Go, structs are the closest equivalent to classes
return entities.structs.map((struct) => ({
name: struct.Name,
location: {
start: { line: struct.StartLine, column: 1 },
end: { line: struct.EndLine, column: 1 }
},
methods: [], // Methods would be found separately
properties: [], // Could be enhanced to extract fields
extends: [], // Go uses composition, not inheritance
implements: [], // Could be enhanced
isExported: struct.IsExported,
isAbstract: false // Go doesn't have abstract structs
}));
}
extractImports(ast) {
// Could be enhanced to extract import information from the Go analyzer
return [];
}
extractExports(ast) {
// Could be enhanced to extract export information from the Go analyzer
return [];
}
// AST Navigation
getParent(node) {
// Simplified implementation - could be enhanced with proper parent tracking
return null;
}
getChildren(node) {
// Simplified implementation - could be enhanced with proper child extraction
return [];
}
// Node Information
getNodeType(node) {
return node.type;
}
getNodeText(node) {
// Extract text representation from the Go entity
const entity = node.raw;
if (entity.Name) {
return entity.Name;
}
return node.type;
}
getNodeLocation(node) {
return node.location;
}
getNodeName(node) {
const entity = node.raw;
return entity.Name || null;
}
// Pattern Matching
isClass(node) {
return node.type === 'struct';
}
isFunction(node) {
return node.type === 'function';
}
isMethod(node) {
return node.type === 'function' && node.raw.Type === 'method';
}
isInterface(node) {
return node.type === 'interface';
}
isImport(node) {
return node.type === 'import';
}
isVariable(node) {
return node.type === 'variable';
}
processJsonBuffer() {
// Look for complete JSON objects (each ends with newline)
const lines = this.jsonBuffer.split('\n');
// Keep the last partial line in buffer (it might be incomplete)
this.jsonBuffer = lines.pop() || '';
// Process complete lines
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed)
continue;
try {
const response = JSON.parse(trimmed);
console.error(`[DEBUG] Go: Received complete response for ID ${response.id}`);
this.writeLog(`Complete JSON-RPC Response for ID ${response.id}: ${JSON.stringify(response).substring(0, 500)}${JSON.stringify(response).length > 500 ? '...[TRUNCATED]' : ''}`);
const pending = this.pendingRequests.get(response.id);
if (pending) {
this.pendingRequests.delete(response.id);
if (response.error) {
console.error(`[DEBUG] Go: Response has error:`, response.error);
this.writeLog(`Response error: ${JSON.stringify(response.error)}`);
pending.reject(new Error(response.error.message));
}
else {
console.error(`[DEBUG] Go: Response successful, resolving with ${JSON.stringify(response.result).length} chars`);
this.writeLog(`Response successful for ID ${response.id}, result size: ${JSON.stringify(response.result).length} chars`);
pending.resolve(response.result);
}
}
else {
console.error(`[DEBUG] Go: No pending request found for ID ${response.id}`);
this.writeLog(`No pending request found for ID ${response.id}`);
}
}
catch (error) {
console.error('[DEBUG] Go: Error parsing JSON line:', error.message, 'Line length:', trimmed.length);
this.writeLog(`JSON Parse Error: ${error.message}, Line length: ${trimmed.length}, Line start: ${trimmed.substring(0, 100)}`);
// If this line can't be parsed, it might be part of a larger JSON object
// Put it back in the buffer and hope the next chunk completes it
if (this.jsonBuffer) {
this.jsonBuffer = trimmed + '\n' + this.jsonBuffer;
}
else {
this.jsonBuffer = trimmed;
}
}
}
}
async cleanup() {
if (this.goAnalyzerProcess) {
console.error('[DEBUG] Go: Cleaning up analyzer process...');
this.writeLog('Cleanup initiated - terminating Go process');
this.goAnalyzerProcess.kill();
this.goAnalyzerProcess = null;
}
// Clear JSON buffer
this.jsonBuffer = '';
// Reject all pending requests
for (const [id, pending] of this.pendingRequests) {
pending.reject(new Error('Adapter cleanup'));
}
this.pendingRequests.clear();
// Reset initialization state
this.isInitialized = false;
this.initializationPromise = null;
this.writeLog('Cleanup completed');
}
}
//# sourceMappingURL=GoAdapter.js.map