universal-ast-mapper 0.5.2 → 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/index.js CHANGED
@@ -46,7 +46,7 @@ function errorText(message) {
46
46
  }
47
47
  const server = new McpServer({
48
48
  name: "universal-ast-mapper",
49
- version: "0.5.2",
49
+ version: "0.5.3",
50
50
  });
51
51
  /* ----------------------- tool: list_supported_languages ----------------------- */
52
52
  server.registerTool("list_supported_languages", {
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;