// NOTE: This mapper invokes a Go helper binary as a subprocess. Use // `execFile` (no shell) with an args array — never `exec` with template // interpolation — so paths containing shell metacharacters are passed safely as // argv entries instead of being parsed by `/bin/sh`. See GH #116. import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { readFile, stat } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import type { FileMap, FileSymbol } from "../types.js"; import { DetailLevel, SymbolKind } from "../enums.js"; export const MAPPER_VERSION = 1; const execFileAsync = promisify(execFile); const __dirname = dirname(fileURLToPath(import.meta.url)); const SCRIPTS_DIR = join(__dirname, "../../../scripts"); const GO_SOURCE = join(SCRIPTS_DIR, "go_outline.go"); const GO_BINARY = join(SCRIPTS_DIR, "go_outline"); interface GoSymbol { name: string; kind: string; startLine: number; endLine: number; signature?: string; modifiers?: string[]; children?: GoSymbol[]; docstring?: string; isExported?: boolean; } interface GoOutlineResult { package: string; imports?: string[]; symbols: GoSymbol[]; error?: string; } // Track if we've already tried to compile let compileAttempted = false; let binaryAvailable = false; /** * Check if Go is available. */ async function hasGo(): Promise { try { await execFileAsync("go", ["version"], { timeout: 5000 }); return true; } catch { return false; } } /** * Ensure the Go binary is compiled. * Returns true if the binary is ready to use. */ async function ensureBinary(): Promise { // Check if binary already exists if (existsSync(GO_BINARY)) { binaryAvailable = true; return true; } // Only try to compile once per session if (compileAttempted) { return binaryAvailable; } compileAttempted = true; // Check if Go is available if (!(await hasGo())) { console.error("Go mapper: go not available for compilation"); return false; } // Check if source exists if (!existsSync(GO_SOURCE)) { console.error(`Go mapper: source not found at ${GO_SOURCE}`); return false; } try { // Compile the binary await execFileAsync("go", ["build", "-o", GO_BINARY, GO_SOURCE], { timeout: 30_000, cwd: SCRIPTS_DIR, }); binaryAvailable = true; return true; } catch (error) { console.error(`Go mapper: compilation failed: ${error}`); return false; } } /** * Map Go kinds to our SymbolKind. */ function mapKind(kind: string): SymbolKind { switch (kind) { case "struct": { return SymbolKind.Class; } case "interface": { return SymbolKind.Interface; } case "function": { return SymbolKind.Function; } case "method": { return SymbolKind.Method; } case "constant": { return SymbolKind.Constant; } case "variable": { return SymbolKind.Variable; } case "type": { return SymbolKind.Type; } case "field": { return SymbolKind.Variable; } default: { return SymbolKind.Unknown; } } } /** * Convert Go symbols to FileSymbols. */ function convertSymbol(gs: GoSymbol): FileSymbol { const symbol: FileSymbol = { name: gs.name, kind: mapKind(gs.kind), startLine: gs.startLine, endLine: gs.endLine, }; if (gs.signature) { symbol.signature = gs.signature; } if (gs.modifiers && gs.modifiers.length > 0) { symbol.modifiers = gs.modifiers; } if (gs.children && gs.children.length > 0) { symbol.children = gs.children.map(convertSymbol); } if (gs.docstring) { symbol.docstring = gs.docstring; } if (gs.isExported !== undefined) { symbol.isExported = gs.isExported; } return symbol; } /** * Generate a file map for a Go file using go/ast. */ export async function goMapper( filePath: string, signal?: AbortSignal ): Promise { try { // Ensure binary is available if (!(await ensureBinary())) { return null; } // Get file stats const stats = await stat(filePath); const totalBytes = stats.size; // Count lines in JS (matches `wc -l` semantics). const fileText = await readFile(filePath, "utf8"); const totalLines = fileText.split("\n").length - 1; // Run Go outline script via execFile (no shell). const { stdout, stderr } = await execFileAsync(GO_BINARY, [filePath], { signal, timeout: 10_000, }); if (stderr && !stdout) { console.error(`Go mapper stderr: ${stderr}`); return null; } const result: GoOutlineResult = JSON.parse(stdout); if (result.error) { console.error(`Go mapper error: ${result.error}`); return null; } const fileMap: FileMap = { path: filePath, totalLines, totalBytes, language: "Go", symbols: result.symbols.map(convertSymbol), imports: result.imports ?? [], detailLevel: DetailLevel.Full, }; return fileMap; } catch (error) { if (signal?.aborted) { return null; } console.error(`Go mapper failed: ${error}`); return null; } }