autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
489 lines (488 loc) • 18.3 kB
JavaScript
/**
* @module GoDiscoverer
* @description Go 项目结构发现器
*
* 检测信号: go.mod, go.sum, *.go
* 支持: 单 Module 项目、Go Workspace (go.work)、标准目录布局 (cmd/ internal/ pkg/)
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { basename, extname, join, relative } from 'node:path';
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
const SOURCE_EXTENSIONS = new Set(['.go']);
const EXCLUDE_DIRS = new Set([
'.git',
'.cursor',
'vendor',
'node_modules',
'testdata',
'.cache',
'dist',
'build',
'_output',
]);
export class GoDiscoverer extends ProjectDiscoverer {
#projectRoot = null;
#targets = [];
#depGraph = { nodes: [], edges: [] };
#modulePath = null;
get id() {
return 'go';
}
get displayName() {
return 'Go (modules)';
}
async detect(projectRoot) {
let confidence = 0;
const reasons = [];
if (existsSync(join(projectRoot, 'go.mod'))) {
confidence = 0.92;
reasons.push('go.mod exists');
}
if (existsSync(join(projectRoot, 'go.sum'))) {
confidence = Math.max(confidence, 0.7);
if (confidence < 0.92) {
confidence += 0.1;
}
reasons.push('go.sum exists');
}
if (existsSync(join(projectRoot, 'go.work'))) {
confidence = Math.max(confidence, 0.95);
reasons.push('go.work exists (workspace)');
}
// 兜底: 根目录有 .go 文件
if (confidence === 0) {
try {
const entries = readdirSync(projectRoot);
if (entries.some((e) => e.endsWith('.go'))) {
confidence = 0.5;
reasons.push('*.go files found at root');
}
}
catch {
/* skip */
}
}
return {
match: confidence > 0,
confidence: Math.min(confidence, 1.0),
reason: reasons.join(', ') || 'No Go markers found',
};
}
async load(projectRoot) {
this.#projectRoot = projectRoot;
this.#targets = [];
this.#depGraph = { nodes: [], edges: [] };
// 解析 go.mod
this.#modulePath = this.#parseGoMod(projectRoot);
const projectName = this.#modulePath
? (this.#modulePath.split('/').pop() ?? basename(projectRoot))
: basename(projectRoot);
// 主 Target — 始终覆盖整个 module(Go 项目根目录递归收集所有 .go 文件)
const framework = this.#detectFramework(projectRoot);
this.#targets.push({
name: projectName,
path: projectRoot,
type: 'library',
language: 'go',
framework,
metadata: { modulePath: this.#modulePath },
});
this.#depGraph.nodes.push(projectName);
// cmd/ 下的子命令作为独立 Target
const cmdTargets = this.#discoverCmdTargets(projectRoot);
for (const t of cmdTargets) {
this.#targets.push(t);
this.#depGraph.nodes.push(t.name);
}
// 检测 test 目录(如果存在独立的 tests/ 或 test/)
for (const testDir of ['test', 'tests', 'e2e']) {
const testPath = join(projectRoot, testDir);
if (existsSync(testPath) && !this.#targets.some((t) => t.name === testDir)) {
this.#targets.push({
name: testDir,
path: testPath,
type: 'test',
language: 'go',
});
}
}
// 发现内部子包(binding/, render/, internal/ 等)
this.#discoverInternalPackages(projectRoot);
// 解析 go.mod 外部依赖(同时添加为 node)
this.#parseDependencies(projectRoot);
// 解析内部 import 关系
this.#parseInternalImports(projectRoot);
}
async listTargets() {
return this.#targets;
}
async getTargetFiles(target) {
const targetPath = typeof target === 'string'
? this.#targets.find((t) => t.name === target)?.path || this.#projectRoot
: target.path;
if (!targetPath || !existsSync(targetPath)) {
return [];
}
const files = [];
this.#collectGoFiles(targetPath, targetPath, files);
return files;
}
async getDependencyGraph() {
return this.#depGraph;
}
// ── 内部实现 ──
/** 解析 go.mod 提取 module path */
#parseGoMod(projectRoot) {
const goModPath = join(projectRoot, 'go.mod');
if (!existsSync(goModPath)) {
return null;
}
try {
const content = readFileSync(goModPath, 'utf8');
const match = content.match(/^module\s+(\S+)/m);
return match ? match[1] : null;
}
catch {
return null;
}
}
/** 发现 Go 标准约定目录: pkg/, internal/, api/ */
#discoverConventionDirs(projectRoot) {
const dirs = [];
const conventionNames = [
{ name: 'pkg', type: 'library' },
{ name: 'internal', type: 'library' },
{ name: 'api', type: 'library' },
{ name: 'server', type: 'application' },
{ name: 'service', type: 'application' },
];
const framework = this.#detectFramework(projectRoot);
for (const conv of conventionNames) {
const dirPath = join(projectRoot, conv.name);
if (existsSync(dirPath)) {
try {
const entries = readdirSync(dirPath, { withFileTypes: true });
const hasGoFiles = entries.some((e) => e.isFile() && e.name.endsWith('.go'));
const hasGoSubDirs = entries.some((e) => e.isDirectory() && !e.name.startsWith('.') && !EXCLUDE_DIRS.has(e.name));
if (hasGoFiles || hasGoSubDirs) {
dirs.push({
name: conv.name,
path: dirPath,
type: conv.type,
language: 'go',
framework,
metadata: { modulePath: this.#modulePath },
});
}
}
catch {
/* skip */
}
}
}
return dirs;
}
/** 发现 cmd/ 下的子命令—每个含 main.go 的子目录为一个 binary Target */
#discoverCmdTargets(projectRoot) {
const cmdDir = join(projectRoot, 'cmd');
if (!existsSync(cmdDir)) {
return [];
}
const targets = [];
const framework = this.#detectFramework(projectRoot);
try {
const entries = readdirSync(cmdDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const subDir = join(cmdDir, entry.name);
targets.push({
name: `cmd/${entry.name}`,
path: subDir,
type: 'application',
language: 'go',
framework,
metadata: { modulePath: this.#modulePath, isCmdBinary: true },
});
}
}
}
catch {
/* skip */
}
// cmd/ 根目录本身有 main.go
if (targets.length === 0) {
try {
const entries = readdirSync(cmdDir);
if (entries.some((e) => e.endsWith('.go'))) {
targets.push({
name: 'cmd',
path: cmdDir,
type: 'application',
language: 'go',
framework,
metadata: { modulePath: this.#modulePath },
});
}
}
catch {
/* skip */
}
}
return targets;
}
/** 检测 Go Web 框架 */
#detectFramework(projectRoot) {
const goModPath = join(projectRoot, 'go.mod');
if (!existsSync(goModPath)) {
return null;
}
try {
const content = readFileSync(goModPath, 'utf8');
if (/github\.com\/gin-gonic\/gin\b/.test(content)) {
return 'gin';
}
if (/github\.com\/labstack\/echo\b/.test(content)) {
return 'echo';
}
if (/github\.com\/gofiber\/fiber\b/.test(content)) {
return 'fiber';
}
if (/github\.com\/gorilla\/mux\b/.test(content)) {
return 'gorilla';
}
if (/github\.com\/beego\b|github\.com\/astaxie\/beego\b/.test(content)) {
return 'beego';
}
if (/google\.golang\.org\/grpc\b/.test(content)) {
return 'grpc';
}
if (/github\.com\/go-chi\/chi\b/.test(content)) {
return 'chi';
}
}
catch {
/* skip */
}
return null;
}
/** 发现内部子包——目录中包含 .go 文件即为一个 Go package */
#discoverInternalPackages(projectRoot) {
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const walk = (dir, relPath, depth) => {
if (depth > 6) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
continue;
}
const subDir = join(dir, entry.name);
const subRel = relPath ? `${relPath}/${entry.name}` : entry.name;
// 检查目录中是否包含 .go 文件
try {
const subEntries = readdirSync(subDir);
const hasGoFiles = subEntries.some((e) => e.endsWith('.go'));
if (hasGoFiles && !nodeSet.has(subRel)) {
this.#depGraph.nodes.push({ id: subRel, label: subRel, type: 'internal' });
nodeSet.add(subRel);
}
}
catch {
/* skip */
}
walk(subDir, subRel, depth + 1);
}
}
catch {
/* skip */
}
};
walk(projectRoot, '', 0);
}
/** 解析 go.mod 依赖到 depGraph(同时将直接依赖添加为 node) */
#parseDependencies(projectRoot) {
const goModPath = join(projectRoot, 'go.mod');
if (!existsSync(goModPath)) {
return;
}
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const rootNode = typeof this.#depGraph.nodes[0] === 'string'
? this.#depGraph.nodes[0]
: this.#depGraph.nodes[0]?.id || 'root';
const addExtDep = (fullPath, indirect) => {
const shortName = fullPath.split('/').pop() ?? fullPath;
// 添加为 node(如果不存在)
if (!nodeSet.has(shortName)) {
this.#depGraph.nodes.push({
id: shortName,
label: shortName,
type: 'external',
fullPath,
indirect,
});
nodeSet.add(shortName);
}
// 添加 edge
this.#depGraph.edges.push({
from: rootNode,
to: shortName,
type: indirect ? 'indirect' : 'dependency',
});
};
try {
const content = readFileSync(goModPath, 'utf8');
// 块 require
const requireBlocks = content.matchAll(/require\s*\(([\s\S]*?)\)/g);
for (const block of requireBlocks) {
const lines = block[1].split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('//')) {
const parts = trimmed.split(/\s+/);
if (parts.length >= 2) {
const indirect = trimmed.includes('// indirect');
addExtDep(parts[0], indirect);
}
}
}
}
// 单行 require(排除 block 语法 `require (`)
const singleRequires = content.matchAll(/^require\s+([^\s(]\S*)\s+\S+/gm);
for (const m of singleRequires) {
const indirect = m[0].includes('// indirect');
addExtDep(m[1], indirect);
}
}
catch {
/* skip */
}
}
/** 解析内部 Go import 语句,构建子包间依赖关系 */
#parseInternalImports(projectRoot) {
if (!this.#modulePath) {
return;
}
const internalNodes = new Set(this.#depGraph.nodes.flatMap((n) => typeof n === 'object' && n.type === 'internal' ? [n.id] : []));
// 也包含根包
const rootNodeId = typeof this.#depGraph.nodes[0] === 'string'
? this.#depGraph.nodes[0]
: (this.#depGraph.nodes[0]?.id ?? '');
const edgeSet = new Set();
const scanPkgImports = (dir, pkgId) => {
try {
const entries = readdirSync(dir);
for (const entry of entries) {
if (!entry.endsWith('.go')) {
continue;
}
try {
const content = readFileSync(join(dir, entry), 'utf8');
// 匹配 import 块和单行 import
const importBlocks = content.matchAll(/import\s*\(([\s\S]*?)\)/g);
for (const block of importBlocks) {
const lines = block[1].split('\n');
for (const line of lines) {
this.#matchInternalImport(line, pkgId, rootNodeId, internalNodes, edgeSet);
}
}
const singleImports = content.matchAll(/^import\s+(?:\w+\s+)?"([^"]+)"/gm);
for (const m of singleImports) {
this.#matchInternalImport(`"${m[1]}"`, pkgId, rootNodeId, internalNodes, edgeSet);
}
}
catch {
/* skip */
}
}
}
catch {
/* skip */
}
};
// 扫描根包
scanPkgImports(projectRoot, rootNodeId);
// 扫描各内部子包
for (const pkgId of internalNodes) {
scanPkgImports(join(projectRoot, pkgId), pkgId);
}
}
/** 从 import 行中匹配内部包引用 */
#matchInternalImport(line, fromPkgId, rootNodeId, internalNodes, edgeSet) {
const match = line.match(/"([^"]+)"/);
if (!match) {
return;
}
const importPath = match[1];
if (!importPath.startsWith(`${this.#modulePath}/`)) {
return;
}
// 去掉 module path 前缀得到相对路径
const relImport = importPath.slice(this.#modulePath.length + 1);
// 确定目标节点
let targetId = null;
if (internalNodes.has(relImport)) {
targetId = relImport;
}
else {
// 可能是子路径,匹配最近的已知包
for (const nodeId of internalNodes) {
if (relImport.startsWith(`${nodeId}/`) || relImport === nodeId) {
targetId = nodeId;
break;
}
}
}
if (targetId && targetId !== fromPkgId) {
const edgeKey = `${fromPkgId}->${targetId}`;
if (!edgeSet.has(edgeKey)) {
edgeSet.add(edgeKey);
this.#depGraph.edges.push({
from: fromPkgId,
to: targetId,
type: 'internal',
});
}
}
}
/** 递归收集 .go 文件 */
#collectGoFiles(dir, rootDir, files, depth = 0) {
if (depth > 15) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
this.#collectGoFiles(join(dir, entry.name), rootDir, files, depth + 1);
}
else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
const fullPath = join(dir, entry.name);
try {
const content = readFileSync(fullPath, 'utf8');
files.push({
name: entry.name,
path: fullPath,
relativePath: relative(rootDir, fullPath),
language: 'go',
content,
});
}
catch {
/* unreadable */
}
}
}
}
catch {
/* permission error */
}
}
}