universal-ast-mapper 1.28.0 → 2.0.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.
Files changed (55) hide show
  1. package/BLUEPRINT.md +230 -230
  2. package/CHANGELOG.md +466 -338
  3. package/README.md +878 -878
  4. package/package.json +48 -47
  5. package/scripts/install-skill.mjs +187 -187
  6. package/dist/analysis.js +0 -134
  7. package/dist/callgraph.js +0 -467
  8. package/dist/check.js +0 -112
  9. package/dist/cli.js +0 -1275
  10. package/dist/complexity.js +0 -98
  11. package/dist/config.js +0 -53
  12. package/dist/contextpack.js +0 -79
  13. package/dist/coupling.js +0 -35
  14. package/dist/crosslang.js +0 -425
  15. package/dist/diskcache.js +0 -97
  16. package/dist/explorer.js +0 -123
  17. package/dist/extractors/c.js +0 -204
  18. package/dist/extractors/common.js +0 -56
  19. package/dist/extractors/cpp.js +0 -272
  20. package/dist/extractors/csharp.js +0 -209
  21. package/dist/extractors/go.js +0 -212
  22. package/dist/extractors/java.js +0 -152
  23. package/dist/extractors/kotlin.js +0 -159
  24. package/dist/extractors/php.js +0 -208
  25. package/dist/extractors/python.js +0 -153
  26. package/dist/extractors/ruby.js +0 -146
  27. package/dist/extractors/rust.js +0 -249
  28. package/dist/extractors/swift.js +0 -192
  29. package/dist/extractors/typescript.js +0 -577
  30. package/dist/gitdiff.js +0 -178
  31. package/dist/graph-analysis.js +0 -279
  32. package/dist/graph.js +0 -165
  33. package/dist/html.js +0 -326
  34. package/dist/index.js +0 -1408
  35. package/dist/layers.js +0 -36
  36. package/dist/modulecoupling.js +0 -0
  37. package/dist/parser.js +0 -84
  38. package/dist/pool.js +0 -114
  39. package/dist/prompts.js +0 -67
  40. package/dist/registry.js +0 -87
  41. package/dist/report.js +0 -232
  42. package/dist/resolver.js +0 -222
  43. package/dist/roots.js +0 -47
  44. package/dist/search.js +0 -68
  45. package/dist/semantic.js +0 -365
  46. package/dist/sfc.js +0 -27
  47. package/dist/skeleton.js +0 -132
  48. package/dist/sourcemap.js +0 -60
  49. package/dist/testmap.js +0 -167
  50. package/dist/tsconfig.js +0 -212
  51. package/dist/typeflow.js +0 -124
  52. package/dist/types.js +0 -5
  53. package/dist/unused-params.js +0 -127
  54. package/dist/worker.js +0 -27
  55. package/dist/workspace.js +0 -330
@@ -1,98 +0,0 @@
1
- import fs from "node:fs";
2
- import { parseSource } from "./parser.js";
3
- import { detectLanguage } from "./registry.js";
4
- import { buildSkeleton } from "./skeleton.js";
5
- import { resolveOptions } from "./config.js";
6
- // ─── Decision points ───────────────────────────────────────────────────────────
7
- /**
8
- * Node types that introduce a branch (each adds 1 to cyclomatic complexity).
9
- * This is a deliberately broad cross-language union; languages that use a node
10
- * type not listed here simply undercount rather than miscount.
11
- */
12
- const DECISION_TYPES = new Set([
13
- // conditionals
14
- "if_statement", "if_expression", "elif_clause", "else_if_clause",
15
- // loops
16
- "for_statement", "for_in_statement", "for_of_statement", "enhanced_for_statement",
17
- "for_expression", "while_statement", "while_expression", "do_statement", "loop_statement",
18
- // switch / match arms (the default/else arm is intentionally excluded)
19
- "switch_case", "expression_case", "type_case", "case_clause", "when_entry",
20
- "when_clause", "match_arm", "case_statement",
21
- // exception handlers
22
- "catch_clause", "except_clause", "rescue_clause",
23
- // ternary
24
- "ternary_expression", "conditional_expression",
25
- // python `and` / `or`
26
- "boolean_operator",
27
- ]);
28
- const FN_KINDS = new Set(["function", "method"]);
29
- function rate(c) {
30
- if (c <= 5)
31
- return "low";
32
- if (c <= 10)
33
- return "moderate";
34
- if (c <= 20)
35
- return "high";
36
- return "very-high";
37
- }
38
- /** Collect the start line of every decision point in the tree. */
39
- function collectDecisionLines(node, out) {
40
- const t = node.type;
41
- if (DECISION_TYPES.has(t)) {
42
- out.push(node.startPosition.row + 1);
43
- }
44
- else if (t === "binary_expression") {
45
- // Short-circuit operators add a branch; arithmetic operators do not.
46
- const op = node.childForFieldName("operator");
47
- if (op && (op.text === "&&" || op.text === "||"))
48
- out.push(node.startPosition.row + 1);
49
- }
50
- for (let i = 0; i < node.namedChildCount; i++) {
51
- const c = node.namedChild(i);
52
- if (c)
53
- collectDecisionLines(c, out);
54
- }
55
- }
56
- function flatten(symbols, acc = []) {
57
- for (const s of symbols) {
58
- acc.push(s);
59
- flatten(s.children, acc);
60
- }
61
- return acc;
62
- }
63
- /**
64
- * Compute cyclomatic complexity for every function/method in a file.
65
- * Complexity is attributed by line range, so a function's score includes the
66
- * control flow of any closures/nested functions declared inside it.
67
- */
68
- export async function computeFileComplexity(absPath, relPath) {
69
- const lang = detectLanguage(absPath);
70
- if (!lang)
71
- return null;
72
- const source = fs.readFileSync(absPath, "utf8");
73
- const root = await parseSource(lang.grammar, source);
74
- const decisionLines = [];
75
- collectDecisionLines(root, decisionLines);
76
- const opts = resolveOptions({ detail: "outline", emitHtml: false });
77
- const skel = await buildSkeleton(absPath, relPath, opts);
78
- const functions = flatten(skel.symbols)
79
- .filter((s) => FN_KINDS.has(s.kind))
80
- .map((s) => {
81
- const count = decisionLines.filter((l) => l >= s.range.startLine && l <= s.range.endLine).length;
82
- const complexity = 1 + count;
83
- return {
84
- name: s.name,
85
- kind: s.kind,
86
- startLine: s.range.startLine,
87
- endLine: s.range.endLine,
88
- complexity,
89
- rating: rate(complexity),
90
- };
91
- })
92
- .sort((a, b) => b.complexity - a.complexity);
93
- const maxComplexity = functions.reduce((m, f) => Math.max(m, f.complexity), 0);
94
- const averageComplexity = functions.length === 0
95
- ? 0
96
- : Math.round((functions.reduce((s, f) => s + f.complexity, 0) / functions.length) * 10) / 10;
97
- return { file: skel.file, functions, maxComplexity, averageComplexity };
98
- }
package/dist/config.js DELETED
@@ -1,53 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- export const DEFAULT_IGNORE = [
4
- "node_modules",
5
- "vendor",
6
- ".git",
7
- "dist",
8
- "build",
9
- ".next",
10
- "out",
11
- "__pycache__",
12
- ".venv",
13
- "venv",
14
- ".ast-map",
15
- ];
16
- let _configCache = null;
17
- /**
18
- * Load .ast-map.config.json from the project root.
19
- * Cached per root path + file mtime so live edits are picked up without restarting.
20
- * Returns an empty config if no file is found.
21
- */
22
- export function loadProjectConfig(root) {
23
- const configPath = path.join(root, ".ast-map.config.json");
24
- let mtime = 0;
25
- try {
26
- mtime = fs.statSync(configPath).mtimeMs;
27
- }
28
- catch { /* file absent */ }
29
- if (_configCache?.root === root && _configCache.mtime === mtime)
30
- return _configCache.config;
31
- let config = {};
32
- try {
33
- const raw = fs.readFileSync(configPath, "utf8");
34
- config = JSON.parse(raw);
35
- }
36
- catch {
37
- // No config file or parse error — use defaults
38
- }
39
- _configCache = { root, mtime, config };
40
- return config;
41
- }
42
- export function resolveOptions(opts = {}, projectConfig = {}) {
43
- const extraIgnore = projectConfig.ignore ?? [];
44
- const mergedIgnore = [...new Set([...DEFAULT_IGNORE, ...extraIgnore])];
45
- return {
46
- detail: opts.detail ?? "outline",
47
- emitHtml: opts.emitHtml ?? true,
48
- combineHtml: opts.combineHtml ?? false,
49
- outputDir: opts.outputDir ?? projectConfig.outputDir,
50
- ignore: opts.ignore ?? mergedIgnore,
51
- maxFileBytes: opts.maxFileBytes ?? projectConfig.maxFileBytes ?? 2_000_000,
52
- };
53
- }
@@ -1,79 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
4
- import { resolveOptions } from "./config.js";
5
- import { resolveFileImports } from "./resolver.js";
6
- import { buildCallGraph } from "./callgraph.js";
7
- function findSym(syms, name) {
8
- for (const s of syms) {
9
- if (s.name === name)
10
- return s;
11
- const n = findSym(s.children, name);
12
- if (n)
13
- return n;
14
- }
15
- return null;
16
- }
17
- const tok = (s) => Math.round(s.length / 4);
18
- /**
19
- * Assemble the minimal context an agent needs to understand or change a symbol:
20
- * the symbol's own source, the signatures of what it depends on (resolved
21
- * imports), and the files that depend on it — instead of reading whole files.
22
- */
23
- export async function packContext(absFile, relFile, root, symbolName, scanDir) {
24
- const opts = resolveOptions({ detail: "full", emitHtml: false });
25
- const skel = await buildSkeleton(absFile, relFile, opts);
26
- const lines = fs.readFileSync(absFile, "utf8").split(/\r?\n/);
27
- let startLine = 1, endLine = lines.length;
28
- if (symbolName) {
29
- const sym = findSym(skel.symbols, symbolName);
30
- if (sym) {
31
- startLine = sym.range.startLine;
32
- endLine = sym.range.endLine;
33
- }
34
- }
35
- const source = lines.slice(startLine - 1, endLine).join("\n");
36
- // Dependencies: resolved in-project imports + the target symbol signatures.
37
- const refs = await resolveFileImports(skel, absFile, root);
38
- const byFile = new Map();
39
- for (const r of refs) {
40
- if (!r.found || !r.resolvedRel)
41
- continue;
42
- const arr = byFile.get(r.resolvedRel) ?? [];
43
- if (!arr.some((x) => x.name === r.symbol))
44
- arr.push({ name: r.symbol, signature: r.signature ?? null });
45
- byFile.set(r.resolvedRel, arr);
46
- }
47
- const dependencies = [...byFile.entries()].map(([file, symbols]) => ({ file, symbols }));
48
- // Dependents: who calls the seed symbol (needs a directory scan).
49
- let dependents = [];
50
- if (symbolName && scanDir) {
51
- const sopts = resolveOptions({ detail: "outline", emitHtml: false });
52
- const skels = [];
53
- for (const f of collectSourceFiles(scanDir, sopts)) {
54
- const rr = path.relative(root, f).split(path.sep).join("/");
55
- try {
56
- skels.push(await buildSkeleton(f, rr, sopts));
57
- }
58
- catch { /* skip */ }
59
- }
60
- const cg = await buildCallGraph(absFile, symbolName, root, skels);
61
- if (cg) {
62
- const seen = new Set();
63
- for (const c of cg.calledBy)
64
- if (!seen.has(c.file)) {
65
- seen.add(c.file);
66
- dependents.push({ file: c.file });
67
- }
68
- }
69
- }
70
- const depTok = dependencies.reduce((a, d) => a + d.symbols.reduce((b, s) => b + tok(s.signature || s.name), 0), 0);
71
- return {
72
- seed: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}) },
73
- primary: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}), startLine, endLine, source },
74
- dependencies,
75
- dependents,
76
- tokenEstimate: tok(source) + depTok,
77
- note: "Read primary.source in full; for dependencies you usually only need the listed signatures, not the whole files.",
78
- };
79
- }
package/dist/coupling.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Compute Robert C. Martin's coupling metrics per file from the symbol graph's
3
- * file-level import edges: afferent (fan-in), efferent (fan-out), and instability.
4
- */
5
- export function computeCoupling(graph) {
6
- const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
7
- const out = new Map(); // file -> set of files it imports
8
- const inc = new Map(); // file -> set of files that import it
9
- const files = new Set();
10
- for (const n of graph.nodes)
11
- if (n.nodeType === "file")
12
- files.add(n.id);
13
- for (const e of graph.edges) {
14
- if (e.edgeType !== "imports")
15
- continue;
16
- const to = nodeMap.get(e.to);
17
- const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
18
- const fromFile = e.from;
19
- if (!toFile || fromFile === toFile)
20
- continue;
21
- files.add(fromFile);
22
- files.add(toFile);
23
- (out.get(fromFile) ?? out.set(fromFile, new Set()).get(fromFile)).add(toFile);
24
- (inc.get(toFile) ?? inc.set(toFile, new Set()).get(toFile)).add(fromFile);
25
- }
26
- const metrics = [];
27
- for (const f of files) {
28
- const ce = out.get(f)?.size ?? 0;
29
- const ca = inc.get(f)?.size ?? 0;
30
- const instability = ca + ce === 0 ? 0 : Math.round((ce / (ca + ce)) * 100) / 100;
31
- metrics.push({ file: f, afferent: ca, efferent: ce, instability });
32
- }
33
- // Sort by total coupling desc (most-connected first).
34
- return metrics.sort((a, b) => (b.afferent + b.efferent) - (a.afferent + a.efferent) || a.file.localeCompare(b.file));
35
- }
package/dist/crosslang.js DELETED
@@ -1,425 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const TYPE_KINDS = new Set(["class", "interface", "enum", "struct"]);
4
- /** Walk a symbol tree and yield each top-level type-like symbol. */
5
- function topTypeSymbols(symbols) {
6
- return symbols.filter((s) => TYPE_KINDS.has(s.kind));
7
- }
8
- function getDirectiveValue(skel, prefix) {
9
- const hit = (skel.directives ?? []).find((d) => d.startsWith(prefix));
10
- return hit ? hit.slice(prefix.length) : null;
11
- }
12
- function getAllDirectiveValues(skel, prefix) {
13
- return (skel.directives ?? [])
14
- .filter((d) => d.startsWith(prefix))
15
- .map((d) => d.slice(prefix.length));
16
- }
17
- /**
18
- * Build Java + C# indexes from already-parsed skeletons.
19
- * Cheap: O(symbols), no extra file reads.
20
- */
21
- export function buildCrossLangIndex(skeletons) {
22
- const index = {
23
- javaFqcn: new Map(),
24
- javaPackages: new Map(),
25
- csharpNamespaces: new Map(),
26
- csharpTypes: new Map(),
27
- kotlinFqcn: new Map(),
28
- kotlinPackages: new Map(),
29
- swiftModules: new Map(),
30
- };
31
- for (const skel of skeletons) {
32
- if (skel.language === "java") {
33
- const pkg = getDirectiveValue(skel, "package:");
34
- if (!pkg)
35
- continue;
36
- const pkgFiles = index.javaPackages.get(pkg) ?? [];
37
- pkgFiles.push(skel.file);
38
- index.javaPackages.set(pkg, pkgFiles);
39
- for (const sym of topTypeSymbols(skel.symbols)) {
40
- index.javaFqcn.set(`${pkg}.${sym.name}`, skel.file);
41
- }
42
- }
43
- else if (skel.language === "csharp") {
44
- const namespaces = getAllDirectiveValues(skel, "namespace:");
45
- for (const ns of namespaces) {
46
- const arr = index.csharpNamespaces.get(ns) ?? [];
47
- if (!arr.includes(skel.file))
48
- arr.push(skel.file);
49
- index.csharpNamespaces.set(ns, arr);
50
- }
51
- // Map every top-level type to <ns>.<TypeName>. For files with multiple
52
- // namespaces this is approximate (we don't know per-symbol scoping
53
- // without per-symbol namespace tracking) but accurate for the common case
54
- // of one namespace per file.
55
- for (const sym of topTypeSymbols(skel.symbols)) {
56
- for (const ns of namespaces) {
57
- index.csharpTypes.set(`${ns}.${sym.name}`, skel.file);
58
- }
59
- }
60
- }
61
- else if (skel.language === "kotlin") {
62
- const pkg = getDirectiveValue(skel, "package:");
63
- if (!pkg)
64
- continue;
65
- const pkgFiles = index.kotlinPackages.get(pkg) ?? [];
66
- pkgFiles.push(skel.file);
67
- index.kotlinPackages.set(pkg, pkgFiles);
68
- for (const sym of topTypeSymbols(skel.symbols)) {
69
- index.kotlinFqcn.set(`${pkg}.${sym.name}`, skel.file);
70
- }
71
- }
72
- else if (skel.language === "swift") {
73
- const mod = swiftModuleOf(skel.file);
74
- if (!mod)
75
- continue;
76
- const arr = index.swiftModules.get(mod) ?? [];
77
- arr.push(skel.file);
78
- index.swiftModules.set(mod, arr);
79
- }
80
- }
81
- return index;
82
- }
83
- /**
84
- * Derive a Swift module name from a project-relative file path.
85
- * SwiftPM convention: sources live under `Sources/<ModuleName>/...`, so the
86
- * module is the path segment right after `Sources`. For flat layouts we fall
87
- * back to the immediate parent directory name. Returns null for bare files.
88
- */
89
- function swiftModuleOf(relFile) {
90
- const parts = relFile.split("/");
91
- const srcIdx = parts.lastIndexOf("Sources");
92
- if (srcIdx >= 0 && srcIdx + 1 < parts.length - 1)
93
- return parts[srcIdx + 1];
94
- if (parts.length >= 2)
95
- return parts[parts.length - 2];
96
- return null;
97
- }
98
- /* ─── Rust module resolution ──────────────────────────────────────────────── */
99
- function findCargoRoot(fromAbs, projectRoot) {
100
- let dir = path.dirname(fromAbs);
101
- const stop = path.resolve(projectRoot);
102
- while (true) {
103
- if (fs.existsSync(path.join(dir, "Cargo.toml")))
104
- return dir;
105
- if (dir === stop || dir === path.dirname(dir))
106
- return null;
107
- dir = path.dirname(dir);
108
- }
109
- }
110
- function existsFile(p) {
111
- try {
112
- return fs.statSync(p).isFile();
113
- }
114
- catch {
115
- return false;
116
- }
117
- }
118
- function existsDir(p) {
119
- try {
120
- return fs.statSync(p).isDirectory();
121
- }
122
- catch {
123
- return false;
124
- }
125
- }
126
- /** Walk module path segments down from a base dir, returning the resolved .rs file. */
127
- function walkRustModule(base, segs) {
128
- if (segs.length === 0) {
129
- // base IS the target module dir/file. Try canonical roots.
130
- for (const candidate of ["mod.rs", "lib.rs", "main.rs"]) {
131
- const p = path.join(base, candidate);
132
- if (existsFile(p))
133
- return p;
134
- }
135
- if (existsFile(base + ".rs"))
136
- return base + ".rs";
137
- return null;
138
- }
139
- const [head, ...rest] = segs;
140
- if (rest.length === 0) {
141
- // Terminal segment: `head.rs` or `head/mod.rs`.
142
- const p1 = path.join(base, `${head}.rs`);
143
- if (existsFile(p1))
144
- return p1;
145
- const p2 = path.join(base, head, "mod.rs");
146
- if (existsFile(p2))
147
- return p2;
148
- return null;
149
- }
150
- // Non-terminal: descend into a directory module.
151
- const subDir = path.join(base, head);
152
- if (existsDir(subDir))
153
- return walkRustModule(subDir, rest);
154
- // Rust-2018 style: `head.rs` next to a sibling `head/` directory.
155
- if (existsFile(path.join(base, `${head}.rs`)) && existsDir(path.join(base, head))) {
156
- return walkRustModule(path.join(base, head), rest);
157
- }
158
- return null;
159
- }
160
- /**
161
- * Resolve a Rust `use` path to an absolute .rs file.
162
- * Handles `crate::`, `self::`, `super::` prefixes; anything else is treated
163
- * as an external crate (e.g. `std::`, `tokio::`) and returns null.
164
- *
165
- * @param importFrom Full use path, e.g. "crate::foo::Bar"
166
- * @param fromAbs Absolute path of the importing file
167
- * @param projectRoot Absolute project root (security boundary)
168
- */
169
- export function resolveRustModule(importFrom, fromAbs, projectRoot) {
170
- const segs = importFrom.split("::");
171
- if (segs.length === 0)
172
- return null;
173
- // Strip the imported item name (last segment) — leaves the module path.
174
- const moduleSegs = segs.slice(0, -1);
175
- if (moduleSegs.length === 0)
176
- return null;
177
- const head = moduleSegs[0];
178
- let base;
179
- let remaining;
180
- if (head === "crate") {
181
- const cargoRoot = findCargoRoot(fromAbs, projectRoot);
182
- if (!cargoRoot)
183
- return null;
184
- base = path.join(cargoRoot, "src");
185
- if (!existsDir(base))
186
- base = cargoRoot;
187
- remaining = moduleSegs.slice(1);
188
- }
189
- else if (head === "self") {
190
- base = path.dirname(fromAbs);
191
- remaining = moduleSegs.slice(1);
192
- }
193
- else if (head === "super") {
194
- let dir = path.dirname(fromAbs);
195
- let i = 0;
196
- while (moduleSegs[i] === "super") {
197
- dir = path.dirname(dir);
198
- i++;
199
- }
200
- base = dir;
201
- remaining = moduleSegs.slice(i);
202
- }
203
- else {
204
- return null; // external crate
205
- }
206
- return walkRustModule(base, remaining);
207
- }
208
- // Cache per project root.
209
- const goModuleCache = new Map();
210
- function readGoModulePath(modFile) {
211
- try {
212
- const txt = fs.readFileSync(modFile, "utf8");
213
- const m = /^\s*module\s+(\S+)/m.exec(txt);
214
- return m ? m[1] : null;
215
- }
216
- catch {
217
- return null;
218
- }
219
- }
220
- /** Locate the go.mod ancestor (within projectRoot) of `fromAbs`. */
221
- function findGoModule(fromAbs, projectRoot) {
222
- const key = path.resolve(projectRoot);
223
- if (goModuleCache.has(key)) {
224
- // Cached at the project root level — assumes one go.mod per project root.
225
- // For monorepos with multiple modules, ResolveGoImport falls back to a
226
- // walk-up search below.
227
- const cached = goModuleCache.get(key);
228
- if (cached)
229
- return cached;
230
- }
231
- let dir = path.dirname(fromAbs);
232
- const stop = key;
233
- while (true) {
234
- const modFile = path.join(dir, "go.mod");
235
- if (existsFile(modFile)) {
236
- const modPath = readGoModulePath(modFile);
237
- if (modPath) {
238
- const result = { modulePath: modPath, moduleDir: dir };
239
- if (dir === stop)
240
- goModuleCache.set(key, result);
241
- return result;
242
- }
243
- }
244
- if (dir === stop || dir === path.dirname(dir))
245
- return null;
246
- dir = path.dirname(dir);
247
- }
248
- }
249
- /**
250
- * Resolve a Go import path to a list of .go files in the resolved package
251
- * directory. Returns null for stdlib / third-party / unresolvable paths.
252
- *
253
- * Go semantics:
254
- * - A package is a directory containing one or more .go files.
255
- * - `import "github.com/x/y/z"` maps to <moduleDir>/<subpath> when the prefix
256
- * matches the current module's path.
257
- */
258
- export function resolveGoImport(importFrom, fromAbs, projectRoot) {
259
- const mod = findGoModule(fromAbs, projectRoot);
260
- if (!mod)
261
- return null;
262
- let subPath;
263
- if (importFrom === mod.modulePath) {
264
- subPath = "";
265
- }
266
- else if (importFrom.startsWith(mod.modulePath + "/")) {
267
- subPath = importFrom.slice(mod.modulePath.length + 1);
268
- }
269
- else {
270
- return null; // external / stdlib
271
- }
272
- const pkgDir = subPath ? path.join(mod.moduleDir, subPath) : mod.moduleDir;
273
- if (!existsDir(pkgDir))
274
- return null;
275
- // Collect every .go file in the directory (Go packages span all files in dir).
276
- // Skip _test.go files — call graph cares about production code.
277
- let entries;
278
- try {
279
- entries = fs.readdirSync(pkgDir, { withFileTypes: true });
280
- }
281
- catch {
282
- return null;
283
- }
284
- const files = [];
285
- for (const e of entries) {
286
- if (!e.isFile())
287
- continue;
288
- if (!e.name.endsWith(".go"))
289
- continue;
290
- if (e.name.endsWith("_test.go"))
291
- continue;
292
- const abs = path.join(pkgDir, e.name);
293
- const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
294
- files.push(rel);
295
- }
296
- return files.length > 0 ? files : null;
297
- }
298
- /** Test/debug hook: drop the cached Go module info. */
299
- export function clearGoModuleCache() {
300
- goModuleCache.clear();
301
- }
302
- /* ─── C / C++ #include resolution ─────────────────────────────────────────── */
303
- const HEADER_EXTS = [".h", ".hpp", ".hxx", ".hh"];
304
- const IMPL_EXTS = [".c", ".cpp", ".cc", ".cxx"];
305
- /**
306
- * Resolve a C/C++ `#include "foo.h"` to in-project files.
307
- * Convention: also pair foo.h with foo.c/.cpp in the same directory so the
308
- * graph captures the header → impl relationship.
309
- * `#include <foo.h>` (system headers) returns null (external).
310
- */
311
- export function resolveCInclude(importFrom, fromAbs, projectRoot) {
312
- // System headers like stdio.h, vector, etc. — leave to external.
313
- const isSystemHeader = !importFrom.includes("/") && !importFrom.includes(".") ||
314
- /^(stdio|stdlib|string|vector|memory|cstdint|cstdlib|cstring|iostream)/.test(importFrom);
315
- // We only check the actual filesystem; if a system header happens to exist
316
- // locally we still link it, otherwise it falls through to null.
317
- const fromDir = path.dirname(fromAbs);
318
- const headerAbs = path.resolve(fromDir, importFrom);
319
- const out = [];
320
- if (existsFile(headerAbs)) {
321
- const rel = path.relative(projectRoot, headerAbs).split(path.sep).join("/");
322
- // Reject paths that escape the project root.
323
- if (!rel.startsWith(".."))
324
- out.push(rel);
325
- // Pair foo.h with foo.{c,cpp,cc,cxx} in the same directory.
326
- const ext = path.extname(headerAbs).toLowerCase();
327
- if (HEADER_EXTS.includes(ext)) {
328
- const base = headerAbs.slice(0, -ext.length);
329
- for (const implExt of IMPL_EXTS) {
330
- const implAbs = base + implExt;
331
- if (existsFile(implAbs)) {
332
- const implRel = path.relative(projectRoot, implAbs).split(path.sep).join("/");
333
- if (!implRel.startsWith(".."))
334
- out.push(implRel);
335
- }
336
- }
337
- }
338
- }
339
- if (isSystemHeader && out.length === 0)
340
- return null;
341
- return out.length > 0 ? out : null;
342
- }
343
- /**
344
- * Resolve an ImportRef in a non-relative-path language to a graph target.
345
- * Returns null for unresolvable / external imports.
346
- */
347
- export function resolveCrossLangTarget(imp, skel, fromAbs, projectRoot, index) {
348
- if (skel.language === "java") {
349
- if (imp.symbol === "*") {
350
- // wildcard: pull all files in the package
351
- const files = index.javaPackages.get(imp.from);
352
- if (files && files.length > 0)
353
- return { kind: "file", files: files.slice() };
354
- return null;
355
- }
356
- const targetFile = index.javaFqcn.get(imp.from);
357
- if (targetFile)
358
- return { kind: "symbol", file: targetFile, symbol: imp.symbol };
359
- return null;
360
- }
361
- if (skel.language === "csharp") {
362
- const files = index.csharpNamespaces.get(imp.from);
363
- if (files && files.length > 0) {
364
- // Exclude self — a file `using App;` while declaring in App shouldn't self-edge.
365
- const filtered = files.filter((f) => f !== skel.file);
366
- if (filtered.length > 0)
367
- return { kind: "file", files: filtered };
368
- }
369
- return null;
370
- }
371
- if (skel.language === "rust") {
372
- const abs = resolveRustModule(imp.from, fromAbs, projectRoot);
373
- if (!abs)
374
- return null;
375
- const rel = path.relative(projectRoot, abs).split(path.sep).join("/");
376
- return { kind: "symbol", file: rel, symbol: imp.symbol };
377
- }
378
- if (skel.language === "go") {
379
- const files = resolveGoImport(imp.from, fromAbs, projectRoot);
380
- if (!files || files.length === 0)
381
- return null;
382
- const filtered = files.filter((f) => f !== skel.file);
383
- if (filtered.length === 0)
384
- return null;
385
- return { kind: "file", files: filtered };
386
- }
387
- if (skel.language === "kotlin") {
388
- if (imp.symbol === "*") {
389
- const files = index.kotlinPackages.get(imp.from);
390
- if (files && files.length > 0) {
391
- const filtered = files.filter((f) => f !== skel.file);
392
- if (filtered.length > 0)
393
- return { kind: "file", files: filtered };
394
- }
395
- return null;
396
- }
397
- const targetFile = index.kotlinFqcn.get(imp.from);
398
- if (targetFile && targetFile !== skel.file) {
399
- return { kind: "symbol", file: targetFile, symbol: imp.symbol };
400
- }
401
- return null;
402
- }
403
- if (skel.language === "c" || skel.language === "cpp") {
404
- const files = resolveCInclude(imp.from, fromAbs, projectRoot);
405
- if (!files || files.length === 0)
406
- return null;
407
- const filtered = files.filter((f) => f !== skel.file);
408
- if (filtered.length === 0)
409
- return null;
410
- return { kind: "file", files: filtered };
411
- }
412
- if (skel.language === "swift") {
413
- // `import <Module>` brings in another in-project module's public symbols
414
- // (no symbol named). Resolve to that module's files; unknown modules
415
- // (Foundation, UIKit, …) are external.
416
- const files = index.swiftModules.get(imp.from);
417
- if (files && files.length > 0) {
418
- const filtered = files.filter((f) => f !== skel.file);
419
- if (filtered.length > 0)
420
- return { kind: "file", files: filtered };
421
- }
422
- return null;
423
- }
424
- return null;
425
- }