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.
- package/README.md +31 -6
- package/dist/callgraph.js +266 -86
- package/dist/crosslang.js +312 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/graph.js +77 -38
- package/dist/registry.js +18 -1
- package/dist/resolver.js +117 -60
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
86
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
112
|
-
|
|
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
|
}
|