@ctxo/lang-go 0.7.0 → 0.8.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/index.d.ts +69 -2
- package/dist/index.js +483 -11
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/tools/ctxo-go-analyzer/go.mod +10 -0
- package/tools/ctxo-go-analyzer/go.sum +8 -0
- package/tools/ctxo-go-analyzer/internal/edges/edges.go +303 -0
- package/tools/ctxo-go-analyzer/internal/edges/edges_test.go +180 -0
- package/tools/ctxo-go-analyzer/internal/emit/emit.go +189 -0
- package/tools/ctxo-go-analyzer/internal/emit/emit_test.go +84 -0
- package/tools/ctxo-go-analyzer/internal/extends/extends.go +138 -0
- package/tools/ctxo-go-analyzer/internal/extends/extends_test.go +141 -0
- package/tools/ctxo-go-analyzer/internal/implements/implements.go +115 -0
- package/tools/ctxo-go-analyzer/internal/implements/implements_test.go +141 -0
- package/tools/ctxo-go-analyzer/internal/load/load.go +67 -0
- package/tools/ctxo-go-analyzer/internal/reach/reach.go +332 -0
- package/tools/ctxo-go-analyzer/internal/reach/reach_test.go +142 -0
- package/tools/ctxo-go-analyzer/internal/symbols/symbols.go +172 -0
- package/tools/ctxo-go-analyzer/internal/symbols/symbols_test.go +159 -0
- package/tools/ctxo-go-analyzer/main.go +180 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,420 @@
|
|
|
1
|
+
// src/analyzer/analyzer-adapter.ts
|
|
2
|
+
import { relative } from "path";
|
|
3
|
+
|
|
4
|
+
// src/logger.ts
|
|
5
|
+
function enabledFor(namespace) {
|
|
6
|
+
const env = process.env["DEBUG"];
|
|
7
|
+
if (!env) return false;
|
|
8
|
+
const patterns = env.split(",").map((p) => p.trim()).filter(Boolean);
|
|
9
|
+
for (const pattern of patterns) {
|
|
10
|
+
if (pattern === "*" || pattern === namespace) return true;
|
|
11
|
+
if (pattern.endsWith("*") && namespace.startsWith(pattern.slice(0, -1))) return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function emit(namespace, level, message, args) {
|
|
16
|
+
const line = `[${namespace}] ${message}${args.length ? " " + args.map(String).join(" ") : ""}`;
|
|
17
|
+
if (level === "error") process.stderr.write(line + "\n");
|
|
18
|
+
else if (level === "warn") process.stderr.write(line + "\n");
|
|
19
|
+
else if (enabledFor(namespace)) process.stderr.write(line + "\n");
|
|
20
|
+
}
|
|
21
|
+
function createLogger(namespace) {
|
|
22
|
+
return {
|
|
23
|
+
debug: (msg, ...args) => emit(namespace, "debug", msg, args),
|
|
24
|
+
info: (msg, ...args) => emit(namespace, "info", msg, args),
|
|
25
|
+
warn: (msg, ...args) => emit(namespace, "warn", msg, args),
|
|
26
|
+
error: (msg, ...args) => emit(namespace, "error", msg, args)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/analyzer/toolchain-detect.ts
|
|
31
|
+
import { execFileSync } from "child_process";
|
|
32
|
+
var log = createLogger("ctxo:lang-go");
|
|
33
|
+
var MIN_MAJOR = 1;
|
|
34
|
+
var MIN_MINOR = 22;
|
|
35
|
+
function detectGoToolchain() {
|
|
36
|
+
try {
|
|
37
|
+
const out = execFileSync("go", ["version"], { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
38
|
+
const match = out.match(/go(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
39
|
+
if (!match) {
|
|
40
|
+
log.info(`Could not parse go version output: ${out}`);
|
|
41
|
+
return { available: false };
|
|
42
|
+
}
|
|
43
|
+
const major = parseInt(match[1], 10);
|
|
44
|
+
const minor = parseInt(match[2], 10);
|
|
45
|
+
const version = match[3] ? `${major}.${minor}.${match[3]}` : `${major}.${minor}`;
|
|
46
|
+
if (major < MIN_MAJOR || major === MIN_MAJOR && minor < MIN_MINOR) {
|
|
47
|
+
log.info(`go ${version} found but >= ${MIN_MAJOR}.${MIN_MINOR} required`);
|
|
48
|
+
return { available: false, version };
|
|
49
|
+
}
|
|
50
|
+
return { available: true, version };
|
|
51
|
+
} catch {
|
|
52
|
+
return { available: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/analyzer/module-discovery.ts
|
|
57
|
+
import { existsSync, readdirSync } from "fs";
|
|
58
|
+
import { dirname, join, resolve } from "path";
|
|
59
|
+
var log2 = createLogger("ctxo:lang-go");
|
|
60
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".ctxo", "vendor", "dist", "build"]);
|
|
61
|
+
function discoverGoModule(rootDir) {
|
|
62
|
+
let dir = resolve(rootDir);
|
|
63
|
+
for (let i = 0; i < 10; i++) {
|
|
64
|
+
if (existsSync(join(dir, "go.work"))) return dir;
|
|
65
|
+
if (existsSync(join(dir, "go.mod"))) return dir;
|
|
66
|
+
const parent = dirname(dir);
|
|
67
|
+
if (parent === dir) break;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
return findShallow(rootDir, 3);
|
|
71
|
+
}
|
|
72
|
+
function findShallow(dir, maxDepth, depth = 0) {
|
|
73
|
+
if (depth > maxDepth) return null;
|
|
74
|
+
try {
|
|
75
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
if (e.isFile() && (e.name === "go.work" || e.name === "go.mod")) {
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const e of entries) {
|
|
82
|
+
if (e.isDirectory() && !IGNORE_DIRS.has(e.name) && !e.name.startsWith(".")) {
|
|
83
|
+
const found = findShallow(join(dir, e.name), maxDepth, depth + 1);
|
|
84
|
+
if (found) return found;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function findPackageRoot(startDir) {
|
|
92
|
+
let dir = startDir;
|
|
93
|
+
for (let i = 0; i < 10; i++) {
|
|
94
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
95
|
+
const parent = dirname(dir);
|
|
96
|
+
if (parent === dir) break;
|
|
97
|
+
dir = parent;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function findMonorepoRoot(startDir) {
|
|
102
|
+
let dir = startDir;
|
|
103
|
+
for (let i = 0; i < 12; i++) {
|
|
104
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
105
|
+
const parent = dirname(dir);
|
|
106
|
+
if (parent === dir) break;
|
|
107
|
+
dir = parent;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function findCtxoGoAnalyzerSource() {
|
|
112
|
+
const pkgRoot = findPackageRoot(import.meta.dirname);
|
|
113
|
+
const monorepoRoot = findMonorepoRoot(import.meta.dirname);
|
|
114
|
+
const candidates = [
|
|
115
|
+
...pkgRoot ? [join(pkgRoot, "tools/ctxo-go-analyzer")] : [],
|
|
116
|
+
...monorepoRoot ? [join(monorepoRoot, "packages/lang-go/tools/ctxo-go-analyzer")] : [],
|
|
117
|
+
join(import.meta.dirname, "../tools/ctxo-go-analyzer"),
|
|
118
|
+
join(import.meta.dirname, "../../tools/ctxo-go-analyzer"),
|
|
119
|
+
join(import.meta.dirname, "../../../tools/ctxo-go-analyzer"),
|
|
120
|
+
join(process.cwd(), "node_modules/@ctxo/lang-go/tools/ctxo-go-analyzer")
|
|
121
|
+
];
|
|
122
|
+
for (const candidate of candidates) {
|
|
123
|
+
if (existsSync(join(candidate, "go.mod")) && existsSync(join(candidate, "main.go"))) {
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
log2.info("ctxo-go-analyzer source not found in any candidate location");
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/analyzer/binary-build.ts
|
|
132
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
133
|
+
import { createHash } from "crypto";
|
|
134
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync } from "fs";
|
|
135
|
+
import { homedir, platform } from "os";
|
|
136
|
+
import { join as join2 } from "path";
|
|
137
|
+
var log3 = createLogger("ctxo:lang-go");
|
|
138
|
+
var BINARY_NAME = platform() === "win32" ? "ctxo-go-analyzer.exe" : "ctxo-go-analyzer";
|
|
139
|
+
var BUILD_TIMEOUT_MS = 18e4;
|
|
140
|
+
function ensureAnalyzerBinary(sourceDir, goVersion) {
|
|
141
|
+
const key = `${hashSourceTree(sourceDir)}-go${goVersion}`;
|
|
142
|
+
const cacheDir = join2(homedir(), ".cache", "ctxo", "lang-go-analyzer", key);
|
|
143
|
+
const binaryPath = join2(cacheDir, BINARY_NAME);
|
|
144
|
+
if (existsSync2(binaryPath)) return binaryPath;
|
|
145
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
146
|
+
log3.info(`Building ctxo-go-analyzer (first run): ${binaryPath}`);
|
|
147
|
+
try {
|
|
148
|
+
execFileSync2("go", ["build", "-trimpath", "-o", binaryPath, "."], {
|
|
149
|
+
cwd: sourceDir,
|
|
150
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
151
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new Error(`go build failed: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
if (!existsSync2(binaryPath)) {
|
|
157
|
+
throw new Error("go build completed but expected binary is missing");
|
|
158
|
+
}
|
|
159
|
+
return binaryPath;
|
|
160
|
+
}
|
|
161
|
+
function hashSourceTree(dir) {
|
|
162
|
+
const hash = createHash("sha1");
|
|
163
|
+
const files = [];
|
|
164
|
+
walk(dir, (path) => {
|
|
165
|
+
if (path.endsWith(".go") || path.endsWith("go.mod") || path.endsWith("go.sum")) {
|
|
166
|
+
files.push(path);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
files.sort();
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
hash.update(f);
|
|
172
|
+
hash.update(readFileSync(f));
|
|
173
|
+
}
|
|
174
|
+
return hash.digest("hex").slice(0, 12);
|
|
175
|
+
}
|
|
176
|
+
function walk(dir, cb) {
|
|
177
|
+
let entries;
|
|
178
|
+
try {
|
|
179
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const path = join2(dir, String(entry.name));
|
|
185
|
+
if (entry.isDirectory()) walk(path, cb);
|
|
186
|
+
else if (entry.isFile()) cb(path);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/analyzer/analyzer-process.ts
|
|
191
|
+
import { spawn } from "child_process";
|
|
192
|
+
var log4 = createLogger("ctxo:lang-go");
|
|
193
|
+
async function runBatchAnalyze(binaryPath, moduleRoot, timeoutMs = 12e4) {
|
|
194
|
+
return new Promise((resolve2) => {
|
|
195
|
+
const empty = {
|
|
196
|
+
files: [],
|
|
197
|
+
dead: [],
|
|
198
|
+
hasMain: false,
|
|
199
|
+
timeout: false,
|
|
200
|
+
totalFiles: 0,
|
|
201
|
+
elapsed: ""
|
|
202
|
+
};
|
|
203
|
+
const proc = spawn(binaryPath, ["--root", moduleRoot], {
|
|
204
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
205
|
+
timeout: timeoutMs
|
|
206
|
+
});
|
|
207
|
+
const files = [];
|
|
208
|
+
let dead = [];
|
|
209
|
+
let hasMain = false;
|
|
210
|
+
let timeout = false;
|
|
211
|
+
let totalFiles = 0;
|
|
212
|
+
let elapsed = "";
|
|
213
|
+
let stderr = "";
|
|
214
|
+
let buffer = "";
|
|
215
|
+
proc.stdout.on("data", (chunk) => {
|
|
216
|
+
buffer += chunk.toString();
|
|
217
|
+
let nl;
|
|
218
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
219
|
+
const line = buffer.slice(0, nl).trim();
|
|
220
|
+
buffer = buffer.slice(nl + 1);
|
|
221
|
+
if (!line) continue;
|
|
222
|
+
try {
|
|
223
|
+
const obj = JSON.parse(line);
|
|
224
|
+
switch (obj.type) {
|
|
225
|
+
case "file":
|
|
226
|
+
files.push(obj);
|
|
227
|
+
break;
|
|
228
|
+
case "dead":
|
|
229
|
+
dead = Array.isArray(obj.symbolIds) ? obj.symbolIds : [];
|
|
230
|
+
hasMain = Boolean(obj.hasMain);
|
|
231
|
+
timeout = Boolean(obj.timeout);
|
|
232
|
+
break;
|
|
233
|
+
case "progress":
|
|
234
|
+
log4.info(String(obj.message ?? ""));
|
|
235
|
+
break;
|
|
236
|
+
case "summary":
|
|
237
|
+
totalFiles = Number(obj.totalFiles ?? 0);
|
|
238
|
+
elapsed = String(obj.elapsed ?? "");
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
log4.error(`Failed to parse JSONL line: ${line.slice(0, 120)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
proc.stderr.on("data", (chunk) => {
|
|
247
|
+
stderr += chunk.toString();
|
|
248
|
+
});
|
|
249
|
+
proc.on("close", (code) => {
|
|
250
|
+
if (code !== 0) {
|
|
251
|
+
log4.error(`ctxo-go-analyzer exited with code ${code}: ${stderr.trim().slice(0, 500)}`);
|
|
252
|
+
resolve2(empty);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
resolve2({ files, dead, hasMain, timeout, totalFiles, elapsed });
|
|
256
|
+
});
|
|
257
|
+
proc.on("error", (err) => {
|
|
258
|
+
log4.error(`ctxo-go-analyzer spawn error: ${err.message}`);
|
|
259
|
+
resolve2(empty);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/analyzer/analyzer-adapter.ts
|
|
265
|
+
var log5 = createLogger("ctxo:lang-go");
|
|
266
|
+
var VALID_SYMBOL_ID = /^.+::.+::.+$/;
|
|
267
|
+
var GoAnalyzerAdapter = class {
|
|
268
|
+
extensions = [".go"];
|
|
269
|
+
tier = "full";
|
|
270
|
+
moduleRoot = null;
|
|
271
|
+
binaryPath = null;
|
|
272
|
+
modulePathPrefix = "";
|
|
273
|
+
cache = /* @__PURE__ */ new Map();
|
|
274
|
+
deadSymbolIds = /* @__PURE__ */ new Set();
|
|
275
|
+
hasMain = false;
|
|
276
|
+
timedOut = false;
|
|
277
|
+
batchPromise = null;
|
|
278
|
+
initialized = false;
|
|
279
|
+
isSupported(filePath) {
|
|
280
|
+
return filePath.endsWith(".go");
|
|
281
|
+
}
|
|
282
|
+
isReady() {
|
|
283
|
+
return this.initialized && this.binaryPath !== null && this.moduleRoot !== null;
|
|
284
|
+
}
|
|
285
|
+
async initialize(rootDir) {
|
|
286
|
+
const toolchain = detectGoToolchain();
|
|
287
|
+
if (!toolchain.available) {
|
|
288
|
+
log5.info(`Go analyzer unavailable: go ${toolchain.version ?? "not found"} (>= 1.22 required)`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const moduleRoot = discoverGoModule(rootDir);
|
|
292
|
+
if (!moduleRoot) {
|
|
293
|
+
log5.info("Go analyzer unavailable: no go.mod or go.work found");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.moduleRoot = moduleRoot;
|
|
297
|
+
const prefix = relative(rootDir, moduleRoot).replace(/\\/g, "/");
|
|
298
|
+
this.modulePathPrefix = prefix && prefix !== "." ? prefix : "";
|
|
299
|
+
const sourceDir = findCtxoGoAnalyzerSource();
|
|
300
|
+
if (!sourceDir) {
|
|
301
|
+
log5.info("Go analyzer unavailable: ctxo-go-analyzer source not located");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
this.binaryPath = ensureAnalyzerBinary(sourceDir, toolchain.version);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log5.warn(`Go analyzer binary build failed: ${err.message}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
log5.info(`Go analyzer ready: go ${toolchain.version}, module ${moduleRoot}`);
|
|
311
|
+
this.initialized = true;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Returns the dead-symbol set emitted by the last batch run. Empty until
|
|
315
|
+
* extractSymbols/Edges has been called at least once. Consumed by cli's
|
|
316
|
+
* find_dead_code MCP tool via the composite delegate.
|
|
317
|
+
*/
|
|
318
|
+
getDeadSymbolIds() {
|
|
319
|
+
return this.deadSymbolIds;
|
|
320
|
+
}
|
|
321
|
+
/** True when the analyzer ran in binary mode (precise dead-code). */
|
|
322
|
+
reachabilityHasMain() {
|
|
323
|
+
return this.hasMain;
|
|
324
|
+
}
|
|
325
|
+
/** True when reach analysis exceeded the deadline (degraded precision). */
|
|
326
|
+
reachabilityTimedOut() {
|
|
327
|
+
return this.timedOut;
|
|
328
|
+
}
|
|
329
|
+
async extractSymbols(filePath, _source) {
|
|
330
|
+
if (!this.isReady()) return [];
|
|
331
|
+
await this.ensureBatch();
|
|
332
|
+
const file = this.cache.get(this.normalizePath(filePath));
|
|
333
|
+
if (!file) return [];
|
|
334
|
+
return file.symbols.filter((s) => VALID_SYMBOL_ID.test(s.symbolId)).map((s) => ({
|
|
335
|
+
symbolId: s.symbolId,
|
|
336
|
+
name: s.name,
|
|
337
|
+
kind: s.kind,
|
|
338
|
+
startLine: s.startLine,
|
|
339
|
+
endLine: s.endLine,
|
|
340
|
+
...s.startOffset != null ? { startOffset: s.startOffset } : {},
|
|
341
|
+
...s.endOffset != null ? { endOffset: s.endOffset } : {}
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
async extractEdges(filePath, _source) {
|
|
345
|
+
if (!this.isReady()) return [];
|
|
346
|
+
await this.ensureBatch();
|
|
347
|
+
const file = this.cache.get(this.normalizePath(filePath));
|
|
348
|
+
if (!file) return [];
|
|
349
|
+
return file.edges.filter((e) => VALID_SYMBOL_ID.test(e.from) && VALID_SYMBOL_ID.test(e.to)).map((e) => ({
|
|
350
|
+
from: e.from,
|
|
351
|
+
to: e.to,
|
|
352
|
+
kind: e.kind
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
async extractComplexity(_filePath, _source) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
async dispose() {
|
|
359
|
+
this.cache.clear();
|
|
360
|
+
this.deadSymbolIds.clear();
|
|
361
|
+
this.batchPromise = null;
|
|
362
|
+
this.initialized = false;
|
|
363
|
+
}
|
|
364
|
+
async ensureBatch() {
|
|
365
|
+
if (this.batchPromise) return this.batchPromise;
|
|
366
|
+
this.batchPromise = this.runBatch();
|
|
367
|
+
return this.batchPromise;
|
|
368
|
+
}
|
|
369
|
+
async runBatch() {
|
|
370
|
+
if (!this.binaryPath || !this.moduleRoot) return;
|
|
371
|
+
const result = await runBatchAnalyze(this.binaryPath, this.moduleRoot);
|
|
372
|
+
this.absorbBatch(result);
|
|
373
|
+
log5.info(`Go analyzer batch: ${result.totalFiles} files in ${result.elapsed}, ${result.dead.length} dead`);
|
|
374
|
+
}
|
|
375
|
+
absorbBatch(result) {
|
|
376
|
+
this.cache.clear();
|
|
377
|
+
this.deadSymbolIds.clear();
|
|
378
|
+
this.hasMain = result.hasMain;
|
|
379
|
+
this.timedOut = result.timeout;
|
|
380
|
+
const rewrite = this.buildPathRewriter();
|
|
381
|
+
for (const file of result.files) {
|
|
382
|
+
const projectRel = rewrite(file.file);
|
|
383
|
+
this.cache.set(projectRel, {
|
|
384
|
+
...file,
|
|
385
|
+
file: projectRel,
|
|
386
|
+
symbols: file.symbols.map((s) => ({ ...s, symbolId: rewriteId(s.symbolId, file.file, projectRel) })),
|
|
387
|
+
edges: file.edges.map((e) => ({
|
|
388
|
+
...e,
|
|
389
|
+
from: rewriteId(e.from, file.file, projectRel),
|
|
390
|
+
to: rewriteIdAcrossModule(e.to, this.modulePathPrefix)
|
|
391
|
+
}))
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
for (const id of result.dead) {
|
|
395
|
+
this.deadSymbolIds.add(rewriteIdAcrossModule(id, this.modulePathPrefix));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
normalizePath(filePath) {
|
|
399
|
+
return filePath.replace(/\\/g, "/");
|
|
400
|
+
}
|
|
401
|
+
buildPathRewriter() {
|
|
402
|
+
if (!this.modulePathPrefix) return (p) => p;
|
|
403
|
+
const prefix = this.modulePathPrefix;
|
|
404
|
+
return (p) => `${prefix}/${p}`;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function rewriteId(id, analyzerFile, projectFile) {
|
|
408
|
+
if (analyzerFile === projectFile) return id;
|
|
409
|
+
return id.startsWith(`${analyzerFile}::`) ? `${projectFile}${id.slice(analyzerFile.length)}` : id;
|
|
410
|
+
}
|
|
411
|
+
function rewriteIdAcrossModule(id, prefix) {
|
|
412
|
+
if (!prefix) return id;
|
|
413
|
+
const sepIdx = id.indexOf("::");
|
|
414
|
+
if (sepIdx <= 0) return id;
|
|
415
|
+
return `${prefix}/${id}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
1
418
|
// src/go-adapter.ts
|
|
2
419
|
import GoLanguage from "tree-sitter-go";
|
|
3
420
|
|
|
@@ -49,6 +466,7 @@ var TreeSitterAdapter = class {
|
|
|
49
466
|
};
|
|
50
467
|
|
|
51
468
|
// src/go-adapter.ts
|
|
469
|
+
var log6 = createLogger("ctxo:lang-go");
|
|
52
470
|
var GO_BRANCH_TYPES = [
|
|
53
471
|
"if_statement",
|
|
54
472
|
"for_statement",
|
|
@@ -72,7 +490,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
72
490
|
const node = tree.rootNode.child(i);
|
|
73
491
|
if (node.type === "function_declaration") {
|
|
74
492
|
const name = node.childForFieldName("name")?.text;
|
|
75
|
-
if (!name
|
|
493
|
+
if (!name) continue;
|
|
76
494
|
const range = this.nodeToLineRange(node);
|
|
77
495
|
symbols.push({
|
|
78
496
|
symbolId: this.buildSymbolId(filePath, name, "function"),
|
|
@@ -83,7 +501,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
83
501
|
}
|
|
84
502
|
if (node.type === "method_declaration") {
|
|
85
503
|
const methodName = node.childForFieldName("name")?.text;
|
|
86
|
-
if (!methodName
|
|
504
|
+
if (!methodName) continue;
|
|
87
505
|
const receiverType = this.extractReceiverType(node);
|
|
88
506
|
const qualifiedName = receiverType ? `${receiverType}.${methodName}` : methodName;
|
|
89
507
|
const range = this.nodeToLineRange(node);
|
|
@@ -100,7 +518,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
100
518
|
}
|
|
101
519
|
return symbols;
|
|
102
520
|
} catch (err) {
|
|
103
|
-
|
|
521
|
+
log6.error(`Symbol extraction failed for ${filePath}: ${err.message}`);
|
|
104
522
|
return [];
|
|
105
523
|
}
|
|
106
524
|
}
|
|
@@ -118,7 +536,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
118
536
|
}
|
|
119
537
|
return edges;
|
|
120
538
|
} catch (err) {
|
|
121
|
-
|
|
539
|
+
log6.error(`Edge extraction failed for ${filePath}: ${err.message}`);
|
|
122
540
|
return [];
|
|
123
541
|
}
|
|
124
542
|
}
|
|
@@ -130,7 +548,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
130
548
|
const node = tree.rootNode.child(i);
|
|
131
549
|
if (node.type === "function_declaration") {
|
|
132
550
|
const name = node.childForFieldName("name")?.text;
|
|
133
|
-
if (!name
|
|
551
|
+
if (!name) continue;
|
|
134
552
|
metrics.push({
|
|
135
553
|
symbolId: this.buildSymbolId(filePath, name, "function"),
|
|
136
554
|
cyclomatic: this.countCyclomaticComplexity(node, GO_BRANCH_TYPES)
|
|
@@ -138,7 +556,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
138
556
|
}
|
|
139
557
|
if (node.type === "method_declaration") {
|
|
140
558
|
const methodName = node.childForFieldName("name")?.text;
|
|
141
|
-
if (!methodName
|
|
559
|
+
if (!methodName) continue;
|
|
142
560
|
const receiverType = this.extractReceiverType(node);
|
|
143
561
|
const qualifiedName = receiverType ? `${receiverType}.${methodName}` : methodName;
|
|
144
562
|
metrics.push({
|
|
@@ -149,7 +567,7 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
149
567
|
}
|
|
150
568
|
return metrics;
|
|
151
569
|
} catch (err) {
|
|
152
|
-
|
|
570
|
+
log6.error(`Complexity extraction failed for ${filePath}: ${err.message}`);
|
|
153
571
|
return [];
|
|
154
572
|
}
|
|
155
573
|
}
|
|
@@ -235,22 +653,76 @@ var GoAdapter = class extends TreeSitterAdapter {
|
|
|
235
653
|
}
|
|
236
654
|
};
|
|
237
655
|
|
|
656
|
+
// src/composite-adapter.ts
|
|
657
|
+
var log7 = createLogger("ctxo:lang-go");
|
|
658
|
+
var GoCompositeAdapter = class {
|
|
659
|
+
analyzer = null;
|
|
660
|
+
treeSitter;
|
|
661
|
+
constructor() {
|
|
662
|
+
this.treeSitter = new GoAdapter();
|
|
663
|
+
}
|
|
664
|
+
async initialize(rootDir) {
|
|
665
|
+
try {
|
|
666
|
+
const analyzer = new GoAnalyzerAdapter();
|
|
667
|
+
await analyzer.initialize(rootDir);
|
|
668
|
+
if (analyzer.isReady()) {
|
|
669
|
+
this.analyzer = analyzer;
|
|
670
|
+
log7.info("Go plugin: ctxo-go-analyzer full-tier active");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
await analyzer.dispose();
|
|
674
|
+
} catch (err) {
|
|
675
|
+
log7.warn(`Go analyzer unavailable: ${err.message}`);
|
|
676
|
+
}
|
|
677
|
+
log7.info("Go plugin: tree-sitter syntax-tier active (install Go 1.22+ for full tier)");
|
|
678
|
+
}
|
|
679
|
+
async dispose() {
|
|
680
|
+
if (this.analyzer) await this.analyzer.dispose();
|
|
681
|
+
}
|
|
682
|
+
extractSymbols(filePath, source) {
|
|
683
|
+
return (this.analyzer ?? this.treeSitter).extractSymbols(filePath, source);
|
|
684
|
+
}
|
|
685
|
+
extractEdges(filePath, source) {
|
|
686
|
+
return (this.analyzer ?? this.treeSitter).extractEdges(filePath, source);
|
|
687
|
+
}
|
|
688
|
+
extractComplexity(filePath, source) {
|
|
689
|
+
return this.treeSitter.extractComplexity(filePath, source);
|
|
690
|
+
}
|
|
691
|
+
isSupported(filePath) {
|
|
692
|
+
return filePath.toLowerCase().endsWith(".go");
|
|
693
|
+
}
|
|
694
|
+
setSymbolRegistry(registry) {
|
|
695
|
+
this.treeSitter.setSymbolRegistry?.(registry);
|
|
696
|
+
}
|
|
697
|
+
/** Exposed for cli optimizations. Null when running in syntax tier. */
|
|
698
|
+
getAnalyzerDelegate() {
|
|
699
|
+
return this.analyzer;
|
|
700
|
+
}
|
|
701
|
+
getTier() {
|
|
702
|
+
if (this.analyzer) return "full";
|
|
703
|
+
if (this.treeSitter) return "syntax";
|
|
704
|
+
return "unavailable";
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
238
708
|
// src/index.ts
|
|
239
|
-
var VERSION = "0.
|
|
709
|
+
var VERSION = "0.8.0-alpha.0";
|
|
240
710
|
var plugin = {
|
|
241
711
|
apiVersion: "1",
|
|
242
712
|
id: "go",
|
|
243
|
-
name: "Go (tree-sitter)",
|
|
713
|
+
name: "Go (ctxo-go-analyzer + tree-sitter)",
|
|
244
714
|
version: VERSION,
|
|
245
715
|
extensions: [".go"],
|
|
246
|
-
tier: "
|
|
716
|
+
tier: "full",
|
|
247
717
|
createAdapter(_ctx) {
|
|
248
|
-
return new
|
|
718
|
+
return new GoCompositeAdapter();
|
|
249
719
|
}
|
|
250
720
|
};
|
|
251
721
|
var index_default = plugin;
|
|
252
722
|
export {
|
|
253
723
|
GoAdapter,
|
|
724
|
+
GoAnalyzerAdapter,
|
|
725
|
+
GoCompositeAdapter,
|
|
254
726
|
TreeSitterAdapter,
|
|
255
727
|
index_default as default,
|
|
256
728
|
plugin
|