universal-ast-mapper 0.5.3 → 0.7.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.
@@ -0,0 +1,249 @@
1
+ import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
2
+ import { makeSymbol } from "./common.js";
3
+ /* ─── helpers ─────────────────────────────────────────────────────────────── */
4
+ function childOfType(node, type) {
5
+ for (let i = 0; i < node.childCount; i++) {
6
+ const c = node.child(i);
7
+ if (c && c.type === type)
8
+ return c;
9
+ }
10
+ return null;
11
+ }
12
+ /** Rust: `pub`, `pub(crate)`, ... = public; anything else = module-private. */
13
+ function isPub(node) {
14
+ const vis = childOfType(node, "visibility_modifier");
15
+ return !!vis && vis.text.startsWith("pub");
16
+ }
17
+ function vis(node) {
18
+ return isPub(node) ? "public" : "private";
19
+ }
20
+ /* ─── import extraction ───────────────────────────────────────────────────── */
21
+ export function extractImportsRust(root, _source) {
22
+ const out = [];
23
+ for (const child of namedChildren(root)) {
24
+ if (child.type === "use_declaration") {
25
+ const arg = child.namedChild(0);
26
+ if (arg)
27
+ resolveUse(arg, "", out);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ function resolveUse(node, prefix, out) {
33
+ switch (node.type) {
34
+ case "identifier":
35
+ case "type_identifier":
36
+ out.push({ symbol: node.text, from: join(prefix, node.text) });
37
+ return;
38
+ case "scoped_identifier": {
39
+ const full = node.text;
40
+ const sym = full.split("::").pop() ?? full;
41
+ out.push({ symbol: sym, from: full });
42
+ return;
43
+ }
44
+ case "use_as_clause": {
45
+ const path = node.namedChild(0);
46
+ const alias = node.namedChild(1);
47
+ const full = path ? path.text : "";
48
+ const sym = full.split("::").pop() ?? full;
49
+ out.push({ symbol: sym, from: full, alias: alias?.text });
50
+ return;
51
+ }
52
+ case "use_wildcard": {
53
+ const path = node.namedChild(0);
54
+ out.push({ symbol: "*", from: path ? path.text : prefix, isNamespaceImport: true });
55
+ return;
56
+ }
57
+ case "scoped_use_list": {
58
+ const base = node.namedChild(0);
59
+ const list = childOfType(node, "use_list");
60
+ const newPrefix = base ? base.text : prefix;
61
+ if (list) {
62
+ for (const item of namedChildren(list))
63
+ resolveUse(item, newPrefix, out);
64
+ }
65
+ return;
66
+ }
67
+ case "use_list":
68
+ for (const item of namedChildren(node))
69
+ resolveUse(item, prefix, out);
70
+ return;
71
+ default:
72
+ return;
73
+ }
74
+ }
75
+ function join(prefix, name) {
76
+ return prefix ? `${prefix}::${name}` : name;
77
+ }
78
+ /* ─── symbol extraction ───────────────────────────────────────────────────── */
79
+ export function extractRust(root, _source) {
80
+ return collect(namedChildren(root));
81
+ }
82
+ function collect(nodes) {
83
+ const out = [];
84
+ for (const n of nodes) {
85
+ const res = handle(n);
86
+ if (Array.isArray(res))
87
+ out.push(...res);
88
+ else if (res)
89
+ out.push(res);
90
+ }
91
+ return out;
92
+ }
93
+ function handle(node) {
94
+ switch (node.type) {
95
+ case "struct_item": {
96
+ const name = nameOf(node) ?? "(struct)";
97
+ const body = node.childForFieldName("body");
98
+ return makeSymbol({
99
+ name,
100
+ kind: "struct",
101
+ node,
102
+ rawKind: node.type,
103
+ visibility: vis(node),
104
+ exported: isPub(node),
105
+ doc: leadingComment(node),
106
+ children: body && body.type === "field_declaration_list" ? structFields(body) : [],
107
+ });
108
+ }
109
+ case "trait_item": {
110
+ const name = nameOf(node) ?? "(trait)";
111
+ const body = node.childForFieldName("body");
112
+ return makeSymbol({
113
+ name,
114
+ kind: "interface",
115
+ node,
116
+ rawKind: node.type,
117
+ visibility: vis(node),
118
+ exported: isPub(node),
119
+ doc: leadingComment(node),
120
+ children: body ? traitMethods(body) : [],
121
+ });
122
+ }
123
+ case "enum_item":
124
+ return makeSymbol({
125
+ name: nameOf(node) ?? "(enum)",
126
+ kind: "enum",
127
+ node,
128
+ rawKind: node.type,
129
+ visibility: vis(node),
130
+ exported: isPub(node),
131
+ doc: leadingComment(node),
132
+ });
133
+ case "function_item": {
134
+ const name = nameOf(node) ?? "(fn)";
135
+ return makeSymbol({
136
+ name,
137
+ kind: "function",
138
+ node,
139
+ rawKind: node.type,
140
+ signature: headerSignature(node, node.childForFieldName("body")),
141
+ visibility: vis(node),
142
+ exported: isPub(node),
143
+ doc: leadingComment(node),
144
+ });
145
+ }
146
+ case "impl_item": {
147
+ // Surface impl methods as `Type::method` so the association is visible.
148
+ const typeNode = node.childForFieldName("type");
149
+ const typeName = typeNode ? typeNode.text : "";
150
+ const body = node.childForFieldName("body");
151
+ const out = [];
152
+ if (body) {
153
+ for (const m of namedChildren(body)) {
154
+ if (m.type !== "function_item")
155
+ continue;
156
+ const mName = nameOf(m) ?? "(fn)";
157
+ out.push(makeSymbol({
158
+ name: typeName ? `${typeName}::${mName}` : mName,
159
+ kind: "method",
160
+ node: m,
161
+ rawKind: m.type,
162
+ signature: headerSignature(m, m.childForFieldName("body")),
163
+ visibility: vis(m),
164
+ exported: isPub(m),
165
+ doc: leadingComment(m),
166
+ }));
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ case "const_item":
172
+ return makeSymbol({
173
+ name: nameOf(node) ?? "(const)",
174
+ kind: "const",
175
+ node,
176
+ rawKind: node.type,
177
+ signature: headerSignature(node, node.childForFieldName("value")),
178
+ visibility: vis(node),
179
+ exported: isPub(node),
180
+ });
181
+ case "static_item":
182
+ return makeSymbol({
183
+ name: nameOf(node) ?? "(static)",
184
+ kind: "var",
185
+ node,
186
+ rawKind: node.type,
187
+ signature: headerSignature(node, node.childForFieldName("value")),
188
+ visibility: vis(node),
189
+ exported: isPub(node),
190
+ });
191
+ case "type_item":
192
+ return makeSymbol({
193
+ name: nameOf(node) ?? "(type)",
194
+ kind: "type",
195
+ node,
196
+ rawKind: node.type,
197
+ signature: headerSignature(node, null),
198
+ visibility: vis(node),
199
+ exported: isPub(node),
200
+ });
201
+ case "mod_item": {
202
+ // Flatten module contents to the top level.
203
+ const body = node.childForFieldName("body");
204
+ return body ? collect(namedChildren(body)) : null;
205
+ }
206
+ default:
207
+ return null;
208
+ }
209
+ }
210
+ function structFields(list) {
211
+ const out = [];
212
+ for (const field of namedChildren(list)) {
213
+ if (field.type !== "field_declaration")
214
+ continue;
215
+ const name = nameOf(field);
216
+ if (!name)
217
+ continue;
218
+ out.push(makeSymbol({
219
+ name,
220
+ kind: "field",
221
+ node: field,
222
+ rawKind: field.type,
223
+ signature: field.text.replace(/\s+/g, " ").trim(),
224
+ visibility: vis(field),
225
+ exported: isPub(field),
226
+ }));
227
+ }
228
+ return out;
229
+ }
230
+ function traitMethods(body) {
231
+ const out = [];
232
+ for (const m of namedChildren(body)) {
233
+ if (m.type !== "function_signature_item" && m.type !== "function_item")
234
+ continue;
235
+ const name = nameOf(m);
236
+ if (!name)
237
+ continue;
238
+ out.push(makeSymbol({
239
+ name,
240
+ kind: "method",
241
+ node: m,
242
+ rawKind: m.type,
243
+ signature: headerSignature(m, m.childForFieldName("body")),
244
+ visibility: "public",
245
+ exported: true,
246
+ }));
247
+ }
248
+ return out;
249
+ }
package/dist/graph.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { resolveImportPath } from "./resolver.js";
3
+ import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
3
4
  // ─── Internal helpers ─────────────────────────────────────────────────────────
4
5
  function collectSymbolNodes(symbols, parentId, file, nodes, edges) {
5
6
  for (const sym of symbols) {
6
- // Nested symbols use dot-notation: "src/foo.ts::MyClass.methodName"
7
7
  const nodeId = `${parentId}.${sym.name}`;
8
8
  nodes.push({
9
9
  id: nodeId,
@@ -21,28 +21,70 @@ function collectSymbolNodes(symbols, parentId, file, nodes, edges) {
21
21
  }
22
22
  }
23
23
  }
24
+ // Returns true for path-based import languages (TS/JS/Python/Go).
25
+ function isPathBasedLanguage(language) {
26
+ return (language === "typescript" ||
27
+ language === "tsx" ||
28
+ language === "javascript" ||
29
+ language === "python");
30
+ }
31
+ // Wire one TS/JS/Python-style relative import.
32
+ function wirePathImport(skel, imp, fromFileAbs, root, exportedSymbolMap, edges) {
33
+ if (!imp.from.startsWith("."))
34
+ return;
35
+ if (imp.isSideEffect)
36
+ return;
37
+ const resolvedAbs = resolveImportPath(imp.from, fromFileAbs);
38
+ if (!resolvedAbs)
39
+ return;
40
+ const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
41
+ if (imp.isNamespaceImport || imp.symbol === "*") {
42
+ if (exportedSymbolMap.has(resolvedRel)) {
43
+ edges.push({ from: skel.file, to: resolvedRel, edgeType: "imports" });
44
+ }
45
+ }
46
+ else {
47
+ const fileExports = exportedSymbolMap.get(resolvedRel);
48
+ const targetNodeId = fileExports?.get(imp.symbol);
49
+ if (targetNodeId) {
50
+ edges.push({ from: skel.file, to: targetNodeId, edgeType: "imports" });
51
+ }
52
+ }
53
+ }
54
+ // Wire one cross-language import (Java/C#/Rust) using the project-wide index.
55
+ function wireCrossLangImport(skel, imp, fromFileAbs, root, index, exportedSymbolMap, edges) {
56
+ if (imp.isSideEffect)
57
+ return;
58
+ const target = resolveCrossLangTarget(imp, skel, fromFileAbs, root, index);
59
+ if (!target)
60
+ return;
61
+ if (target.kind === "file") {
62
+ for (const f of target.files) {
63
+ if (exportedSymbolMap.has(f) && f !== skel.file) {
64
+ edges.push({ from: skel.file, to: f, edgeType: "imports" });
65
+ }
66
+ }
67
+ return;
68
+ }
69
+ // target.kind === "symbol"
70
+ const fileExports = exportedSymbolMap.get(target.file);
71
+ const targetNodeId = fileExports?.get(target.symbol);
72
+ if (targetNodeId) {
73
+ edges.push({ from: skel.file, to: targetNodeId, edgeType: "imports" });
74
+ }
75
+ else if (exportedSymbolMap.has(target.file) && target.file !== skel.file) {
76
+ // Symbol not found in the resolved file — fall back to a file-level edge so
77
+ // the graph still reflects the cross-file dependency.
78
+ edges.push({ from: skel.file, to: target.file, edgeType: "imports" });
79
+ }
80
+ }
24
81
  // ─── Public builder ──────────────────────────────────────────────────────────
25
- /**
26
- * Build a symbol-level dependency graph from an array of pre-parsed skeletons.
27
- *
28
- * Node IDs:
29
- * - File node: "<relPath>" e.g. "src/utils.ts"
30
- * - Top symbol: "<relPath>::<Name>" e.g. "src/utils.ts::sanitize"
31
- * - Nested: "<relPath>::<Parent>.<Child>" e.g. "src/utils.ts::MyClass.render"
32
- *
33
- * Edge types:
34
- * - "contains" file → symbol (and parent-symbol → child-symbol)
35
- * - "imports" importing-file → imported-symbol-node
36
- */
37
82
  export function buildSymbolGraph(skeletons, root) {
38
83
  const nodes = [];
39
84
  const edges = [];
40
- // exportedSymbolMap: relFile → (symbolName → nodeId)
41
- // Used in the second pass to resolve import targets.
42
85
  const exportedSymbolMap = new Map();
43
- // ── First pass: build all file + symbol nodes ────────────────────────────
86
+ // First pass: build file and symbol nodes.
44
87
  for (const skel of skeletons) {
45
- // File node
46
88
  nodes.push({
47
89
  id: skel.file,
48
90
  nodeType: "file",
@@ -64,9 +106,15 @@ export function buildSymbolGraph(skeletons, root) {
64
106
  ...(sym.signature ? { signature: sym.signature } : {}),
65
107
  });
66
108
  edges.push({ from: skel.file, to: nodeId, edgeType: "contains" });
109
+ // Index exported (and visible-by-convention) top-level symbols so that
110
+ // imports can find them. Languages like Java/C# may have visible types
111
+ // without an explicit "exported" flag — fall back to indexing all
112
+ // top-level symbols for those languages.
67
113
  if (sym.exported)
68
114
  fileExports.set(sym.name, nodeId);
69
- // Collect nested symbols
115
+ else if (skel.language === "java" || skel.language === "csharp") {
116
+ fileExports.set(sym.name, nodeId);
117
+ }
70
118
  if (sym.children.length > 0) {
71
119
  const childNodes = [];
72
120
  const childEdges = [];
@@ -76,32 +124,23 @@ export function buildSymbolGraph(skeletons, root) {
76
124
  }
77
125
  }
78
126
  }
79
- // ── Second pass: wire import edges ───────────────────────────────────────
127
+ // Build cross-language indexes once (Java FQCN, C# namespaces).
128
+ const crossIndex = buildCrossLangIndex(skeletons);
129
+ // Second pass: wire import edges, dispatched by language.
80
130
  for (const skel of skeletons) {
81
131
  if (!skel.imports || skel.imports.length === 0)
82
132
  continue;
83
133
  const fromFileAbs = path.resolve(root, skel.file);
134
+ const pathBased = isPathBasedLanguage(skel.language);
84
135
  for (const imp of skel.imports) {
85
- if (!imp.from.startsWith("."))
86
- continue; // skip external packages
87
- if (imp.isSideEffect)
88
- continue;
89
- const resolvedAbs = resolveImportPath(imp.from, fromFileAbs);
90
- if (!resolvedAbs)
91
- continue;
92
- const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
93
- if (imp.isNamespaceImport || imp.symbol === "*") {
94
- // Namespace import — link to the file node itself
95
- if (exportedSymbolMap.has(resolvedRel)) {
96
- edges.push({ from: skel.file, to: resolvedRel, edgeType: "imports" });
97
- }
136
+ if (pathBased) {
137
+ wirePathImport(skel, imp, fromFileAbs, root, exportedSymbolMap, edges);
98
138
  }
99
- else {
100
- const fileExports = exportedSymbolMap.get(resolvedRel);
101
- const targetNodeId = fileExports?.get(imp.symbol);
102
- if (targetNodeId) {
103
- edges.push({ from: skel.file, to: targetNodeId, edgeType: "imports" });
104
- }
139
+ else if (skel.language === "java" ||
140
+ skel.language === "csharp" ||
141
+ skel.language === "rust" ||
142
+ skel.language === "go") {
143
+ wireCrossLangImport(skel, imp, fromFileAbs, root, crossIndex, exportedSymbolMap, edges);
105
144
  }
106
145
  }
107
146
  }
package/dist/registry.js CHANGED
@@ -2,6 +2,9 @@ import path from "node:path";
2
2
  import { extractTypeScript, extractDirectivesTS, extractImportsTS } from "./extractors/typescript.js";
3
3
  import { extractPython, extractImportsPython } from "./extractors/python.js";
4
4
  import { extractGo, extractImportsGo } from "./extractors/go.js";
5
+ import { extractRust, extractImportsRust } from "./extractors/rust.js";
6
+ import { extractJava, extractDirectivesJava, extractImportsJava } from "./extractors/java.js";
7
+ import { extractCSharp, extractDirectivesCSharp, extractImportsCSharp } from "./extractors/csharp.js";
5
8
  const TS_ENTRY = (language, grammar) => ({
6
9
  language,
7
10
  grammar,
@@ -9,7 +12,6 @@ const TS_ENTRY = (language, grammar) => ({
9
12
  extractDirectives: extractDirectivesTS,
10
13
  extractImports: extractImportsTS,
11
14
  });
12
- /** Map of file extension -> language entry. The Factory/Strategy registry. */
13
15
  const BY_EXT = {
14
16
  ".ts": TS_ENTRY("typescript", "typescript"),
15
17
  ".mts": TS_ENTRY("typescript", "typescript"),
@@ -22,6 +24,21 @@ const BY_EXT = {
22
24
  ".py": { language: "python", grammar: "python", extract: extractPython, extractImports: extractImportsPython },
23
25
  ".pyi": { language: "python", grammar: "python", extract: extractPython, extractImports: extractImportsPython },
24
26
  ".go": { language: "go", grammar: "go", extract: extractGo, extractImports: extractImportsGo },
27
+ ".rs": { language: "rust", grammar: "rust", extract: extractRust, extractImports: extractImportsRust },
28
+ ".java": {
29
+ language: "java",
30
+ grammar: "java",
31
+ extract: extractJava,
32
+ extractDirectives: extractDirectivesJava,
33
+ extractImports: extractImportsJava,
34
+ },
35
+ ".cs": {
36
+ language: "csharp",
37
+ grammar: "c_sharp",
38
+ extract: extractCSharp,
39
+ extractDirectives: extractDirectivesCSharp,
40
+ extractImports: extractImportsCSharp,
41
+ },
25
42
  };
26
43
  export function detectLanguage(filePath) {
27
44
  return BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
package/dist/resolver.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { buildSkeleton } from "./skeleton.js";
3
+ import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
4
4
  import { resolveOptions } from "./config.js";
5
5
  import { findSymbol } from "./analysis.js";
6
+ import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
6
7
  const SRC_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
7
- /** Extract the outermost balanced parenthesised group from a signature string. */
8
8
  function extractParams(sig) {
9
9
  const start = sig.indexOf("(");
10
10
  if (start === -1)
@@ -21,8 +21,6 @@ function extractParams(sig) {
21
21
  }
22
22
  return null;
23
23
  }
24
- // TypeScript ESM: `import from "./foo.js"` actually means `./foo.ts` on disk.
25
- // Map each JS-family extension to the TS-family equivalents we should try first.
26
24
  const JS_TO_TS = {
27
25
  ".js": [".ts", ".tsx", ".js"],
28
26
  ".jsx": [".tsx", ".jsx"],
@@ -30,8 +28,7 @@ const JS_TO_TS = {
30
28
  ".cjs": [".cts", ".cjs"],
31
29
  };
32
30
  /**
33
- * Resolve a relative import path from a source file to an absolute path on disk.
34
- * Handles TypeScript ESM convention (`.js` in source → `.ts` on disk).
31
+ * Resolve a TS/JS-style relative import path to an absolute file path.
35
32
  * Returns null for external packages or when the file cannot be found.
36
33
  */
37
34
  export function resolveImportPath(importFrom, fromAbs) {
@@ -40,7 +37,6 @@ export function resolveImportPath(importFrom, fromAbs) {
40
37
  const fromDir = path.dirname(fromAbs);
41
38
  const candidate = path.resolve(fromDir, importFrom);
42
39
  const declaredExt = path.extname(candidate).toLowerCase();
43
- // If the import has a JS-family extension, try the TS equivalents first
44
40
  if (declaredExt && JS_TO_TS[declaredExt]) {
45
41
  const base = candidate.slice(0, candidate.length - declaredExt.length);
46
42
  for (const ext of JS_TO_TS[declaredExt]) {
@@ -49,22 +45,17 @@ export function resolveImportPath(importFrom, fromAbs) {
49
45
  return p;
50
46
  }
51
47
  }
52
- // Exact match (already has extension or points to a file)
53
48
  try {
54
49
  const stat = fs.statSync(candidate);
55
50
  if (stat.isFile())
56
51
  return candidate;
57
52
  }
58
- catch {
59
- // not found — try with extensions
60
- }
61
- // Try appending source extensions
53
+ catch { /* not found */ }
62
54
  for (const ext of SRC_EXTS) {
63
55
  const p = candidate + ext;
64
56
  if (fs.existsSync(p))
65
57
  return p;
66
58
  }
67
- // Try index file inside the directory
68
59
  for (const ext of SRC_EXTS) {
69
60
  const p = path.join(candidate, `index${ext}`);
70
61
  if (fs.existsSync(p))
@@ -72,60 +63,126 @@ export function resolveImportPath(importFrom, fromAbs) {
72
63
  }
73
64
  return null;
74
65
  }
75
- /**
76
- * For each import in `skel`, resolve the target file and look up the symbol.
77
- * Returns enriched Reference Objects with resolved path, signature, and params.
78
- */
66
+ /* ─── Cross-language index cache ──────────────────────────────────────────── */
67
+ // Java/C# need a project-wide index to resolve fully-qualified imports.
68
+ // Built lazily on first cross-language resolve, then reused for the process
69
+ // lifetime (the MCP server is per-root, so this is safe).
70
+ const indexCache = new Map();
71
+ export async function getOrBuildCrossLangIndex(root) {
72
+ const key = path.resolve(root);
73
+ let p = indexCache.get(key);
74
+ if (p)
75
+ return p;
76
+ p = (async () => {
77
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
78
+ const files = collectSourceFiles(key, opts);
79
+ const skels = [];
80
+ for (const abs of files) {
81
+ const ext = path.extname(abs).toLowerCase();
82
+ // Only Java/C# contribute to the index (Rust resolves via direct
83
+ // module-path walk against the filesystem, no index needed).
84
+ if (ext !== ".java" && ext !== ".cs")
85
+ continue;
86
+ const rel = path.relative(key, abs).split(path.sep).join("/");
87
+ try {
88
+ skels.push(await buildSkeleton(abs, rel, opts));
89
+ }
90
+ catch { /* skip unparsable files */ }
91
+ }
92
+ return buildCrossLangIndex(skels);
93
+ })();
94
+ indexCache.set(key, p);
95
+ return p;
96
+ }
97
+ /** Test/debug hook: drop the cached index (rebuilds on next call). */
98
+ export function clearCrossLangIndexCache() {
99
+ indexCache.clear();
100
+ }
101
+ async function lookupSymbolInTarget(targetAbs, targetRel, symbol) {
102
+ const opts = resolveOptions({ detail: "full", emitHtml: false });
103
+ try {
104
+ const targetSkel = await buildSkeleton(targetAbs, targetRel, opts);
105
+ const sym = findSymbol(targetSkel.symbols, symbol);
106
+ if (sym) {
107
+ const signature = sym.signature ?? null;
108
+ const out = { found: true, kind: sym.kind };
109
+ if (signature !== undefined)
110
+ out.signature = signature;
111
+ if (signature) {
112
+ const params = extractParams(signature);
113
+ if (params)
114
+ out.params = params;
115
+ }
116
+ return out;
117
+ }
118
+ }
119
+ catch { /* unresolvable / parse error */ }
120
+ return { found: false };
121
+ }
122
+ async function enrichRelativeImport(imp, fromAbs, root) {
123
+ const isExternal = !imp.from.startsWith(".");
124
+ const resolvedAbs = isExternal ? null : resolveImportPath(imp.from, fromAbs);
125
+ const resolvedRel = resolvedAbs
126
+ ? path.relative(root, resolvedAbs).split(path.sep).join("/")
127
+ : null;
128
+ let enrichment = { found: false };
129
+ if (resolvedAbs && !imp.isSideEffect && !imp.isNamespaceImport && imp.symbol !== "*") {
130
+ enrichment = await lookupSymbolInTarget(resolvedAbs, resolvedRel, imp.symbol);
131
+ }
132
+ else if (resolvedAbs) {
133
+ enrichment = { found: true };
134
+ }
135
+ return assembleResolved(imp, resolvedAbs, resolvedRel, isExternal, enrichment);
136
+ }
137
+ async function enrichCrossLangImport(imp, skel, fromAbs, root, index) {
138
+ const target = resolveCrossLangTarget(imp, skel, fromAbs, root, index);
139
+ if (!target) {
140
+ return assembleResolved(imp, null, null, true, { found: false });
141
+ }
142
+ if (target.kind === "file") {
143
+ // Namespace-style (Java wildcard / C# using). Point to the first file —
144
+ // useful for navigation; the symbol itself isn't a specific declaration.
145
+ const firstRel = target.files[0];
146
+ const firstAbs = path.resolve(root, firstRel);
147
+ return assembleResolved(imp, firstAbs, firstRel, false, { found: true });
148
+ }
149
+ // Symbol-level (Java FQCN, Rust crate::path::Item)
150
+ const targetAbs = path.resolve(root, target.file);
151
+ const enrichment = await lookupSymbolInTarget(targetAbs, target.file, target.symbol);
152
+ return assembleResolved(imp, targetAbs, target.file, false, enrichment);
153
+ }
154
+ function assembleResolved(imp, resolvedAbs, resolvedRel, isExternal, enrichment) {
155
+ const out = {
156
+ ...imp,
157
+ resolvedPath: resolvedAbs,
158
+ resolvedRel,
159
+ found: enrichment.found,
160
+ importKind: isExternal ? "external" : "relative",
161
+ };
162
+ if (enrichment.kind !== undefined)
163
+ out.kind = enrichment.kind;
164
+ if (enrichment.signature !== undefined)
165
+ out.signature = enrichment.signature;
166
+ if (enrichment.params !== undefined)
167
+ out.params = enrichment.params;
168
+ return out;
169
+ }
170
+ /* ─── Public entry point ──────────────────────────────────────────────────── */
171
+ const CROSS_LANG = new Set(["java", "csharp", "rust", "go"]);
79
172
  export async function resolveFileImports(skel, absPath, root) {
80
173
  if (!skel.imports || skel.imports.length === 0)
81
174
  return [];
82
- const opts = resolveOptions({ detail: "full", emitHtml: false });
83
175
  const results = [];
176
+ // Lazy-build the cross-lang index only when actually needed.
177
+ let indexPromise = null;
178
+ const getIndex = () => (indexPromise ??= getOrBuildCrossLangIndex(root));
84
179
  for (const imp of skel.imports) {
85
- const isExternal = !imp.from.startsWith(".");
86
- const resolvedAbs = isExternal ? null : resolveImportPath(imp.from, absPath);
87
- const resolvedRel = resolvedAbs
88
- ? path.relative(root, resolvedAbs).split(path.sep).join("/")
89
- : null;
90
- let found = false;
91
- let kind;
92
- let signature;
93
- let params;
94
- if (resolvedAbs && !imp.isSideEffect && !imp.isNamespaceImport && imp.symbol !== "*") {
95
- try {
96
- const targetSkel = await buildSkeleton(resolvedAbs, resolvedRel, opts);
97
- const targetSym = findSymbol(targetSkel.symbols, imp.symbol);
98
- if (targetSym) {
99
- found = true;
100
- kind = targetSym.kind;
101
- signature = targetSym.signature ?? null;
102
- if (signature) {
103
- params = extractParams(signature);
104
- }
105
- }
106
- }
107
- catch {
108
- // target file unresolvable or parse error — leave found=false
109
- }
180
+ if (CROSS_LANG.has(skel.language)) {
181
+ results.push(await enrichCrossLangImport(imp, skel, absPath, root, await getIndex()));
110
182
  }
111
- else if (resolvedAbs) {
112
- // Namespace import or side-effect: file exists = success
113
- found = true;
183
+ else {
184
+ results.push(await enrichRelativeImport(imp, absPath, root));
114
185
  }
115
- const resolved = {
116
- ...imp,
117
- resolvedPath: resolvedAbs,
118
- resolvedRel,
119
- found,
120
- importKind: isExternal ? "external" : "relative",
121
- };
122
- if (kind !== undefined)
123
- resolved.kind = kind;
124
- if (signature !== undefined)
125
- resolved.signature = signature;
126
- if (params !== undefined)
127
- resolved.params = params;
128
- results.push(resolved);
129
186
  }
130
187
  return results;
131
188
  }