import path from "node:path"; import fs from "node:fs"; import type { Database as DBType } from "better-sqlite3"; interface ScanStats { modules: number; symbols: number; } /** 递归收集目录下匹配扩展名的所有文件 */ function walkDir(dir: string, exts: string[]): string[] { if (!fs.existsSync(dir)) return []; const results: string[] = []; const stack: string[] = [dir]; while (stack.length > 0) { const current = stack.pop()!; let entries: fs.Dirent[]; try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { if (["node_modules", "dist", ".git", "vendor"].includes(entry.name)) continue; stack.push(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (exts.includes(ext)) results.push(fullPath); } } } return results; } // Go 符号正则:匹配导出函数、类型、结构体、接口 const GO_FUNC_RE = /^func\s+(?:\([^)]*\)\s+)?([A-Z]\w*)\s*\(/gm; const GO_TYPE_RE = /^type\s+([A-Z]\w*)\s+/gm; const GO_STRUCT_RE = /^type\s+([A-Z]\w*)\s+struct\s*\{/gm; const GO_INTERFACE_RE = /^type\s+([A-Z]\w*)\s+interface\s*\{/gm; export function scanGo(db: DBType, root: string): ScanStats { const insertModule = db.prepare( "INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)", ); const insertSymbol = db.prepare( "INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ); const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as { get: (name: string) => { id: number } | undefined; }; let modules = 0; let symbols = 0; // 扫描 services/* 下有 go.mod 的服务 const servicesDir = path.join(root, "services"); if (!fs.existsSync(servicesDir)) return { modules, symbols }; const entries = fs.readdirSync(servicesDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const serviceName = entry.name; const servicePath = path.join(servicesDir, serviceName); const hasGoMod = fs.existsSync(path.join(servicePath, "go.mod")); if (!hasGoMod) continue; insertModule.run(serviceName, servicePath, "go", serviceName, "service"); const modRow = getModuleId.get(serviceName); if (modRow) { modules++; // 扫描 .go 文件 const goFiles = walkDir(servicePath, [".go"]); for (const filePath of goFiles) { // 跳过测试文件 if (filePath.endsWith("_test.go")) continue; let content: string; try { content = fs.readFileSync(filePath, "utf-8"); } catch { continue; } const lines = content.split("\n"); // 函数 let match: RegExpExecArray | null; const funcRe = new RegExp(GO_FUNC_RE); while ((match = funcRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; insertSymbol.run( modRow.id, name, "function", "go", filePath, lineNum, lineNum, 1, ); symbols++; } // 类型(struct / interface / type alias) const typeRe = new RegExp(GO_TYPE_RE); while ((match = typeRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; // 判断是 struct 还是 interface 还是普通 type const line = lines[lineNum - 1] || ""; let kind = "type"; if (line.includes("struct")) kind = "struct"; else if (line.includes("interface")) kind = "interface"; insertSymbol.run( modRow.id, name, kind, "go", filePath, lineNum, lineNum, 1, ); symbols++; } } } } return { modules, symbols }; }