universal-ast-mapper 0.7.0 → 0.8.1

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 CHANGED
@@ -4,16 +4,18 @@ An **MCP server + CLI tool** that turns source code into structured, machine-rea
4
4
 
5
5
  Built on [tree-sitter](https://tree-sitter.github.io/) WASM grammars. Zero regex guessing — real AST parsing.
6
6
 
7
- **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C#
7
+ **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift
8
8
 
9
- | Capability | TS/JS | Python | Go | Rust | Java | C# |
10
- |--------------------------|:-----:|:------:|:---:|:----:|:----:|:---:|
11
- | Symbol extraction | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
12
- | Imports parsing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
13
- | Graph `imports` edges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
14
- | `resolve_imports` enrich | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
15
- | Call graph callee origin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
16
- | Reverse `calledBy` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
9
+ | Capability | TS/JS | Python | Go | Rust | Java | C# | C | C++ | Kt | Swift |
10
+ |--------------------------|:-----:|:------:|:---:|:----:|:----:|:---:|:---:|:---:|:---:|:-----:|
11
+ | Symbol extraction | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
12
+ | Imports parsing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
13
+ | Graph `imports` edges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
14
+ | `resolve_imports` enrich | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
15
+ | Call graph callee origin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
16
+ | Reverse `calledBy` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
17
+
18
+ > v0.8.0 introduces **symbol extraction + imports parsing** for C / C++ / Kotlin / Swift. Graph/resolver/callgraph dispatch for these four lands in a follow-up release. (Ruby grammar in `tree-sitter-wasms@0.1.13` is unstable and was skipped.)
17
19
 
18
20
  Each language uses the resolution strategy that fits it:
19
21
  - **TS/JS/Python** — relative paths (`./foo`, `..mod`) resolved against the importing file's directory, with TS-ESM `.js` → `.ts` rewriting.
@@ -478,6 +480,8 @@ src/
478
480
 
479
481
  | Version | What changed |
480
482
  |---------|--------------|
483
+ | **0.8.1** | **Cross-file graph wiring for Kotlin & C/C++** — Kotlin FQCN/package index + C/C++ `#include` resolution (with header↔impl pairing) wired into `build_symbol_graph`, `resolve_imports`, and `get_call_graph`. Fixes a parse-cache rel-path leak (stale `.file` poisoned the cross-lang index → doubled paths) and Kotlin call-graph extraction (`function_declaration` name + field-less `call_expression`). |
484
+ | **0.8.0** | **4 new languages: C · C++ · Kotlin · Swift** — symbol extraction + imports parsing. C++ tracks access_specifier through class bodies. Kotlin handles `package`/`object`/`data class`. Swift handles `class`/`struct`/`enum` (all under `class_declaration`) and `protocol_declaration`. Ruby grammar in tree-sitter-wasms@0.1.13 is unstable — skipped. |
481
485
  | **0.7.0** | Go full resolution (reads `go.mod`, resolves package-as-directory) · C# reverse `calledBy` via call-site scanning · `csharpTypes` index lets `using` directives resolve to specific types · 4-suite test harness (smoke + graph-smoke + resolver-smoke + callgraph-smoke) |
482
486
  | **0.6.0** | **3 new languages: Rust · Java · C#** (extractors + import parsing) · cross-language resolver in `crosslang.ts` (Java FQCN index, C# namespace index, Rust `crate::` module walk) · symbol-graph `imports` edges + `resolveFileImports` enrichment + `get_call_graph` callee resolution rewired through it · Java `package` and C# `namespace` captured as directives |
483
487
  | **0.5.3** | Auto-install `/ast-map` Claude Code skill on `npm install` · `postinstall` writes `~/.claude/skills/ast-map/SKILL.md` + registers trigger in `CLAUDE.md` (idempotent, CI-safe) |
package/dist/callgraph.js CHANGED
@@ -6,7 +6,7 @@ import { resolveOptions, loadProjectConfig } from "./config.js";
6
6
  import { detectLanguage } from "./registry.js";
7
7
  import { resolveImportPath, getOrBuildCrossLangIndex } from "./resolver.js";
8
8
  import { resolveCrossLangTarget } from "./crosslang.js";
9
- const CROSS_LANG = new Set(["java", "csharp", "rust", "go"]);
9
+ const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp"]);
10
10
  function pushCall(out, callee, anchor) {
11
11
  if (callee && anchor)
12
12
  out.push({ callee, line: anchor.startPosition.row + 1 });
@@ -54,6 +54,20 @@ function collectCalls(node, out) {
54
54
  }
55
55
  pushCall(out, callee, fn);
56
56
  }
57
+ else {
58
+ // Kotlin: call_expression has no `function` field — the callee is the
59
+ // first named child (a simple_identifier for `Foo(...)` / a bare call,
60
+ // or a navigation_expression for `obj.method(...)`).
61
+ const callee0 = node.namedChild(0);
62
+ if (callee0) {
63
+ if (callee0.type === "simple_identifier" || callee0.type === "identifier") {
64
+ pushCall(out, callee0.text, callee0);
65
+ }
66
+ else if (callee0.type === "navigation_expression") {
67
+ pushCall(out, callee0.text.replace(/\s+/g, ""), callee0);
68
+ }
69
+ }
70
+ }
57
71
  }
58
72
  // ── Java method invocation
59
73
  else if (t === "method_invocation") {
@@ -110,8 +124,16 @@ const FUNCTION_NODE_TYPES = new Set([
110
124
  function findFunctionNode(root, name) {
111
125
  function walk(node) {
112
126
  if (FUNCTION_NODE_TYPES.has(node.type)) {
113
- if (node.childForFieldName("name")?.text === name)
127
+ const named = node.childForFieldName("name");
128
+ if (named?.text === name)
114
129
  return node;
130
+ // Kotlin: function_declaration exposes its name as a simple_identifier
131
+ // child, not via a `name` field.
132
+ if (!named && node.type === "function_declaration") {
133
+ const id = node.namedChild(0);
134
+ if (id?.type === "simple_identifier" && id.text === name)
135
+ return node;
136
+ }
115
137
  }
116
138
  // const foo = () => ... | const foo = function() ...
117
139
  if (node.type === "variable_declarator") {
package/dist/crosslang.js CHANGED
@@ -24,6 +24,8 @@ export function buildCrossLangIndex(skeletons) {
24
24
  javaPackages: new Map(),
25
25
  csharpNamespaces: new Map(),
26
26
  csharpTypes: new Map(),
27
+ kotlinFqcn: new Map(),
28
+ kotlinPackages: new Map(),
27
29
  };
28
30
  for (const skel of skeletons) {
29
31
  if (skel.language === "java") {
@@ -55,6 +57,17 @@ export function buildCrossLangIndex(skeletons) {
55
57
  }
56
58
  }
57
59
  }
60
+ else if (skel.language === "kotlin") {
61
+ const pkg = getDirectiveValue(skel, "package:");
62
+ if (!pkg)
63
+ continue;
64
+ const pkgFiles = index.kotlinPackages.get(pkg) ?? [];
65
+ pkgFiles.push(skel.file);
66
+ index.kotlinPackages.set(pkg, pkgFiles);
67
+ for (const sym of topTypeSymbols(skel.symbols)) {
68
+ index.kotlinFqcn.set(`${pkg}.${sym.name}`, skel.file);
69
+ }
70
+ }
58
71
  }
59
72
  return index;
60
73
  }
@@ -262,6 +275,47 @@ export function resolveGoImport(importFrom, fromAbs, projectRoot) {
262
275
  export function clearGoModuleCache() {
263
276
  goModuleCache.clear();
264
277
  }
278
+ /* ─── C / C++ #include resolution ─────────────────────────────────────────── */
279
+ const HEADER_EXTS = [".h", ".hpp", ".hxx", ".hh"];
280
+ const IMPL_EXTS = [".c", ".cpp", ".cc", ".cxx"];
281
+ /**
282
+ * Resolve a C/C++ `#include "foo.h"` to in-project files.
283
+ * Convention: also pair foo.h with foo.c/.cpp in the same directory so the
284
+ * graph captures the header → impl relationship.
285
+ * `#include <foo.h>` (system headers) returns null (external).
286
+ */
287
+ export function resolveCInclude(importFrom, fromAbs, projectRoot) {
288
+ // System headers like stdio.h, vector, etc. — leave to external.
289
+ const isSystemHeader = !importFrom.includes("/") && !importFrom.includes(".") ||
290
+ /^(stdio|stdlib|string|vector|memory|cstdint|cstdlib|cstring|iostream)/.test(importFrom);
291
+ // We only check the actual filesystem; if a system header happens to exist
292
+ // locally we still link it, otherwise it falls through to null.
293
+ const fromDir = path.dirname(fromAbs);
294
+ const headerAbs = path.resolve(fromDir, importFrom);
295
+ const out = [];
296
+ if (existsFile(headerAbs)) {
297
+ const rel = path.relative(projectRoot, headerAbs).split(path.sep).join("/");
298
+ // Reject paths that escape the project root.
299
+ if (!rel.startsWith(".."))
300
+ out.push(rel);
301
+ // Pair foo.h with foo.{c,cpp,cc,cxx} in the same directory.
302
+ const ext = path.extname(headerAbs).toLowerCase();
303
+ if (HEADER_EXTS.includes(ext)) {
304
+ const base = headerAbs.slice(0, -ext.length);
305
+ for (const implExt of IMPL_EXTS) {
306
+ const implAbs = base + implExt;
307
+ if (existsFile(implAbs)) {
308
+ const implRel = path.relative(projectRoot, implAbs).split(path.sep).join("/");
309
+ if (!implRel.startsWith(".."))
310
+ out.push(implRel);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ if (isSystemHeader && out.length === 0)
316
+ return null;
317
+ return out.length > 0 ? out : null;
318
+ }
265
319
  /**
266
320
  * Resolve an ImportRef in a non-relative-path language to a graph target.
267
321
  * Returns null for unresolvable / external imports.
@@ -301,8 +355,31 @@ export function resolveCrossLangTarget(imp, skel, fromAbs, projectRoot, index) {
301
355
  const files = resolveGoImport(imp.from, fromAbs, projectRoot);
302
356
  if (!files || files.length === 0)
303
357
  return null;
304
- // Exclude self (a file in package X importing X is unusual but possible
305
- // for cyclic / generated cases — don't draw a self-edge).
358
+ const filtered = files.filter((f) => f !== skel.file);
359
+ if (filtered.length === 0)
360
+ return null;
361
+ return { kind: "file", files: filtered };
362
+ }
363
+ if (skel.language === "kotlin") {
364
+ if (imp.symbol === "*") {
365
+ const files = index.kotlinPackages.get(imp.from);
366
+ if (files && files.length > 0) {
367
+ const filtered = files.filter((f) => f !== skel.file);
368
+ if (filtered.length > 0)
369
+ return { kind: "file", files: filtered };
370
+ }
371
+ return null;
372
+ }
373
+ const targetFile = index.kotlinFqcn.get(imp.from);
374
+ if (targetFile && targetFile !== skel.file) {
375
+ return { kind: "symbol", file: targetFile, symbol: imp.symbol };
376
+ }
377
+ return null;
378
+ }
379
+ if (skel.language === "c" || skel.language === "cpp") {
380
+ const files = resolveCInclude(imp.from, fromAbs, projectRoot);
381
+ if (!files || files.length === 0)
382
+ return null;
306
383
  const filtered = files.filter((f) => f !== skel.file);
307
384
  if (filtered.length === 0)
308
385
  return null;
@@ -0,0 +1,204 @@
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
+ /** Recursively unwrap a declarator chain to get the identifier text. */
13
+ function nameFromDeclarator(node) {
14
+ if (!node)
15
+ return null;
16
+ switch (node.type) {
17
+ case "identifier":
18
+ case "field_identifier":
19
+ case "type_identifier":
20
+ return node.text;
21
+ case "pointer_declarator":
22
+ case "array_declarator":
23
+ case "parenthesized_declarator":
24
+ return nameFromDeclarator(node.childForFieldName("declarator"));
25
+ case "function_declarator": {
26
+ const d = node.childForFieldName("declarator");
27
+ return nameFromDeclarator(d);
28
+ }
29
+ case "init_declarator":
30
+ return nameFromDeclarator(node.childForFieldName("declarator"));
31
+ default:
32
+ // best-effort: find first identifier-like child
33
+ for (let i = 0; i < node.namedChildCount; i++) {
34
+ const c = node.namedChild(i);
35
+ if (c && (c.type === "identifier" || c.type === "field_identifier" || c.type === "type_identifier"))
36
+ return c.text;
37
+ }
38
+ return null;
39
+ }
40
+ }
41
+ function hasStaticStorage(node) {
42
+ for (let i = 0; i < node.childCount; i++) {
43
+ const c = node.child(i);
44
+ if (c && c.type === "storage_class_specifier" && c.text === "static")
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ /* ─── imports (#include) ──────────────────────────────────────────────────── */
50
+ export function extractImportsC(root, _source) {
51
+ const out = [];
52
+ for (const child of namedChildren(root)) {
53
+ if (child.type !== "preproc_include")
54
+ continue;
55
+ const pathNode = childOfType(child, "system_lib_string") ?? childOfType(child, "string_literal");
56
+ if (!pathNode)
57
+ continue;
58
+ const raw = pathNode.text.replace(/^[<"]|[>"]$/g, "");
59
+ const base = raw.split("/").pop() ?? raw;
60
+ const sym = base.replace(/\.[hH]$|\.hpp$|\.hxx$|\.hh$/, "");
61
+ out.push({ symbol: sym, from: raw });
62
+ }
63
+ return out;
64
+ }
65
+ /* ─── symbol extraction ───────────────────────────────────────────────────── */
66
+ export function extractC(root, _source) {
67
+ return collect(namedChildren(root));
68
+ }
69
+ function collect(nodes) {
70
+ const out = [];
71
+ for (const n of nodes) {
72
+ const res = handle(n);
73
+ if (Array.isArray(res))
74
+ out.push(...res);
75
+ else if (res)
76
+ out.push(res);
77
+ }
78
+ return out;
79
+ }
80
+ function handle(node) {
81
+ switch (node.type) {
82
+ case "struct_specifier":
83
+ case "union_specifier": {
84
+ const name = nameOf(node);
85
+ if (!name)
86
+ return null;
87
+ const body = node.childForFieldName("body");
88
+ return makeSymbol({
89
+ name,
90
+ kind: "struct",
91
+ node,
92
+ rawKind: node.type,
93
+ doc: leadingComment(node),
94
+ children: body ? structFields(body) : [],
95
+ });
96
+ }
97
+ case "enum_specifier": {
98
+ const name = nameOf(node);
99
+ if (!name)
100
+ return null;
101
+ return makeSymbol({
102
+ name,
103
+ kind: "enum",
104
+ node,
105
+ rawKind: node.type,
106
+ doc: leadingComment(node),
107
+ });
108
+ }
109
+ case "function_definition": {
110
+ const decl = node.childForFieldName("declarator");
111
+ const name = nameFromDeclarator(decl);
112
+ if (!name)
113
+ return null;
114
+ const isStatic = hasStaticStorage(node);
115
+ return makeSymbol({
116
+ name,
117
+ kind: "function",
118
+ node,
119
+ rawKind: node.type,
120
+ signature: headerSignature(node, node.childForFieldName("body")),
121
+ visibility: isStatic ? "private" : "public",
122
+ exported: !isStatic,
123
+ doc: leadingComment(node),
124
+ });
125
+ }
126
+ case "declaration": {
127
+ // top-level variable/function declarations (prototypes, externs, etc.)
128
+ const decl = node.childForFieldName("declarator");
129
+ const name = nameFromDeclarator(decl);
130
+ if (!name)
131
+ return null;
132
+ // skip function prototypes — focus on real defs (function_definition)
133
+ if (decl && containsFunctionDeclarator(decl))
134
+ return null;
135
+ return makeSymbol({
136
+ name,
137
+ kind: "var",
138
+ node,
139
+ rawKind: node.type,
140
+ signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
141
+ visibility: hasStaticStorage(node) ? "private" : "public",
142
+ exported: !hasStaticStorage(node),
143
+ });
144
+ }
145
+ case "preproc_def":
146
+ case "preproc_function_def": {
147
+ const name = nameOf(node);
148
+ if (!name)
149
+ return null;
150
+ return makeSymbol({
151
+ name,
152
+ kind: "const",
153
+ node,
154
+ rawKind: node.type,
155
+ signature: node.text.replace(/\s+/g, " ").trim(),
156
+ });
157
+ }
158
+ case "type_definition": {
159
+ // typedef — name is in the declarator (last identifier)
160
+ const decl = node.childForFieldName("declarator");
161
+ const name = nameFromDeclarator(decl);
162
+ if (!name)
163
+ return null;
164
+ return makeSymbol({
165
+ name,
166
+ kind: "type",
167
+ node,
168
+ rawKind: node.type,
169
+ signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
170
+ });
171
+ }
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+ function containsFunctionDeclarator(node) {
177
+ if (node.type === "function_declarator")
178
+ return true;
179
+ for (let i = 0; i < node.namedChildCount; i++) {
180
+ const c = node.namedChild(i);
181
+ if (c && containsFunctionDeclarator(c))
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ function structFields(body) {
187
+ const out = [];
188
+ for (const field of namedChildren(body)) {
189
+ if (field.type !== "field_declaration")
190
+ continue;
191
+ const decl = field.childForFieldName("declarator");
192
+ const name = nameFromDeclarator(decl);
193
+ if (!name)
194
+ continue;
195
+ out.push(makeSymbol({
196
+ name,
197
+ kind: "field",
198
+ node: field,
199
+ rawKind: field.type,
200
+ signature: field.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
201
+ }));
202
+ }
203
+ return out;
204
+ }
@@ -0,0 +1,272 @@
1
+ import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
2
+ import { makeSymbol } from "./common.js";
3
+ /* ─── helpers (shared with C extractor in spirit but kept local) ──────────── */
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
+ function nameFromDeclarator(node) {
13
+ if (!node)
14
+ return null;
15
+ switch (node.type) {
16
+ case "identifier":
17
+ case "field_identifier":
18
+ case "type_identifier":
19
+ return node.text;
20
+ case "pointer_declarator":
21
+ case "array_declarator":
22
+ case "parenthesized_declarator":
23
+ case "reference_declarator":
24
+ return nameFromDeclarator(node.childForFieldName("declarator") ?? node.namedChild(0));
25
+ case "function_declarator":
26
+ case "init_declarator":
27
+ return nameFromDeclarator(node.childForFieldName("declarator"));
28
+ case "qualified_identifier": {
29
+ // Foo::bar — use "bar" as the leaf name
30
+ const last = node.childForFieldName("name");
31
+ return last ? last.text : node.text;
32
+ }
33
+ case "operator_name":
34
+ case "destructor_name":
35
+ return node.text;
36
+ default:
37
+ for (let i = 0; i < node.namedChildCount; i++) {
38
+ const c = node.namedChild(i);
39
+ if (c && (c.type === "identifier" || c.type === "field_identifier" || c.type === "type_identifier"))
40
+ return c.text;
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ function containsFunctionDeclarator(node) {
46
+ if (node.type === "function_declarator")
47
+ return true;
48
+ for (let i = 0; i < node.namedChildCount; i++) {
49
+ const c = node.namedChild(i);
50
+ if (c && containsFunctionDeclarator(c))
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ function hasStaticStorage(node) {
56
+ for (let i = 0; i < node.childCount; i++) {
57
+ const c = node.child(i);
58
+ if (c && c.type === "storage_class_specifier" && c.text === "static")
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ /* ─── imports (#include) ──────────────────────────────────────────────────── */
64
+ export function extractImportsCpp(root, _source) {
65
+ const out = [];
66
+ const visit = (n) => {
67
+ for (const c of namedChildren(n)) {
68
+ if (c.type === "preproc_include") {
69
+ const p = childOfType(c, "system_lib_string") ?? childOfType(c, "string_literal");
70
+ if (p) {
71
+ const raw = p.text.replace(/^[<"]|[>"]$/g, "");
72
+ const base = raw.split("/").pop() ?? raw;
73
+ out.push({ symbol: base.replace(/\.[hH](pp|xx|h)?$/, ""), from: raw });
74
+ }
75
+ }
76
+ else if (c.type === "namespace_definition" || c.type === "linkage_specification") {
77
+ const body = c.childForFieldName("body");
78
+ if (body)
79
+ visit(body);
80
+ }
81
+ }
82
+ };
83
+ visit(root);
84
+ return out;
85
+ }
86
+ /* ─── symbol extraction ───────────────────────────────────────────────────── */
87
+ export function extractCpp(root, _source) {
88
+ return collect(namedChildren(root));
89
+ }
90
+ function collect(nodes) {
91
+ const out = [];
92
+ for (const n of nodes) {
93
+ const res = handle(n);
94
+ if (Array.isArray(res))
95
+ out.push(...res);
96
+ else if (res)
97
+ out.push(res);
98
+ }
99
+ return out;
100
+ }
101
+ function handle(node) {
102
+ switch (node.type) {
103
+ case "namespace_definition":
104
+ case "linkage_specification": {
105
+ // Recurse into body, flattening namespace contents to top level.
106
+ const body = node.childForFieldName("body");
107
+ return body ? collect(namedChildren(body)) : null;
108
+ }
109
+ case "class_specifier": {
110
+ const name = nameOf(node);
111
+ if (!name)
112
+ return null;
113
+ const body = node.childForFieldName("body");
114
+ return makeSymbol({
115
+ name,
116
+ kind: "class",
117
+ node,
118
+ rawKind: node.type,
119
+ doc: leadingComment(node),
120
+ children: body ? classMembers(body, "private") : [],
121
+ });
122
+ }
123
+ case "struct_specifier": {
124
+ const name = nameOf(node);
125
+ if (!name)
126
+ return null;
127
+ const body = node.childForFieldName("body");
128
+ return makeSymbol({
129
+ name,
130
+ kind: "struct",
131
+ node,
132
+ rawKind: node.type,
133
+ doc: leadingComment(node),
134
+ children: body ? classMembers(body, "public") : [],
135
+ });
136
+ }
137
+ case "enum_specifier": {
138
+ const name = nameOf(node);
139
+ if (!name)
140
+ return null;
141
+ return makeSymbol({
142
+ name,
143
+ kind: "enum",
144
+ node,
145
+ rawKind: node.type,
146
+ doc: leadingComment(node),
147
+ });
148
+ }
149
+ case "function_definition": {
150
+ const decl = node.childForFieldName("declarator");
151
+ const name = nameFromDeclarator(decl);
152
+ if (!name)
153
+ return null;
154
+ return makeSymbol({
155
+ name,
156
+ kind: "function",
157
+ node,
158
+ rawKind: node.type,
159
+ signature: headerSignature(node, node.childForFieldName("body")),
160
+ visibility: hasStaticStorage(node) ? "private" : "public",
161
+ exported: !hasStaticStorage(node),
162
+ doc: leadingComment(node),
163
+ });
164
+ }
165
+ case "template_declaration": {
166
+ // The actual declaration is buried; recurse to find it.
167
+ for (let i = 0; i < node.namedChildCount; i++) {
168
+ const c = node.namedChild(i);
169
+ if (!c)
170
+ continue;
171
+ if (c.type === "class_specifier" ||
172
+ c.type === "struct_specifier" ||
173
+ c.type === "function_definition" ||
174
+ c.type === "declaration") {
175
+ const res = handle(c);
176
+ if (res)
177
+ return res;
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ case "alias_declaration":
183
+ case "type_definition": {
184
+ const decl = node.childForFieldName("declarator");
185
+ const name = nameOf(node) ?? nameFromDeclarator(decl);
186
+ if (!name)
187
+ return null;
188
+ return makeSymbol({
189
+ name,
190
+ kind: "type",
191
+ node,
192
+ rawKind: node.type,
193
+ signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
194
+ });
195
+ }
196
+ case "declaration": {
197
+ const decl = node.childForFieldName("declarator");
198
+ const name = nameFromDeclarator(decl);
199
+ if (!name)
200
+ return null;
201
+ if (decl && containsFunctionDeclarator(decl))
202
+ return null; // prototype
203
+ return makeSymbol({
204
+ name,
205
+ kind: "var",
206
+ node,
207
+ rawKind: node.type,
208
+ signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
209
+ });
210
+ }
211
+ case "preproc_def":
212
+ case "preproc_function_def": {
213
+ const name = nameOf(node);
214
+ if (!name)
215
+ return null;
216
+ return makeSymbol({
217
+ name,
218
+ kind: "const",
219
+ node,
220
+ rawKind: node.type,
221
+ });
222
+ }
223
+ default:
224
+ return null;
225
+ }
226
+ }
227
+ /** Walk class/struct body, tracking the current access_specifier (default given). */
228
+ function classMembers(body, defaultAccess) {
229
+ let current = defaultAccess;
230
+ const out = [];
231
+ for (let i = 0; i < body.childCount; i++) {
232
+ const c = body.child(i);
233
+ if (!c)
234
+ continue;
235
+ if (c.type === "access_specifier") {
236
+ current = /\bpublic\b/.test(c.text) ? "public" : "private";
237
+ continue;
238
+ }
239
+ if (!c.isNamed)
240
+ continue;
241
+ if (c.type === "field_declaration") {
242
+ const dec = c.childForFieldName("declarator");
243
+ const name = nameFromDeclarator(dec);
244
+ if (!name)
245
+ continue;
246
+ const isFunc = dec && containsFunctionDeclarator(dec);
247
+ out.push(makeSymbol({
248
+ name,
249
+ kind: isFunc ? "method" : "field",
250
+ node: c,
251
+ rawKind: c.type,
252
+ signature: c.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
253
+ visibility: current,
254
+ }));
255
+ }
256
+ else if (c.type === "function_definition") {
257
+ const dec = c.childForFieldName("declarator");
258
+ const name = nameFromDeclarator(dec);
259
+ if (!name)
260
+ continue;
261
+ out.push(makeSymbol({
262
+ name,
263
+ kind: "method",
264
+ node: c,
265
+ rawKind: c.type,
266
+ signature: headerSignature(c, c.childForFieldName("body")),
267
+ visibility: current,
268
+ }));
269
+ }
270
+ }
271
+ return out;
272
+ }
@@ -0,0 +1,159 @@
1
+ import { namedChildren, 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
+ function firstChildOfTypes(node, types) {
13
+ for (let i = 0; i < node.namedChildCount; i++) {
14
+ const c = node.namedChild(i);
15
+ if (c && types.has(c.type))
16
+ return c;
17
+ }
18
+ return null;
19
+ }
20
+ const TYPE_NAME_NODES = new Set(["type_identifier", "simple_identifier"]);
21
+ const SIMPLE_NAME_NODES = new Set(["simple_identifier"]);
22
+ function modifiersText(node) {
23
+ const m = childOfType(node, "modifiers");
24
+ return m ? m.text : "";
25
+ }
26
+ function vis(node) {
27
+ const m = modifiersText(node);
28
+ if (/\b(private|protected|internal)\b/.test(m))
29
+ return "private";
30
+ return "public"; // Kotlin default is public
31
+ }
32
+ function exported(node) {
33
+ const m = modifiersText(node);
34
+ if (/\b(private|protected|internal)\b/.test(m))
35
+ return false;
36
+ return true;
37
+ }
38
+ function classKind(node) {
39
+ const m = modifiersText(node);
40
+ if (/\bdata\b/.test(node.text.slice(0, 80)))
41
+ return "class";
42
+ if (/\benum\b/.test(node.text.slice(0, 80)))
43
+ return "enum";
44
+ return "class";
45
+ }
46
+ /* ─── imports + package ───────────────────────────────────────────────────── */
47
+ export function extractDirectivesKotlin(root, _source) {
48
+ for (const c of namedChildren(root)) {
49
+ if (c.type === "package_header") {
50
+ const id = childOfType(c, "identifier");
51
+ if (id)
52
+ return [`package:${id.text}`];
53
+ }
54
+ }
55
+ return [];
56
+ }
57
+ export function extractImportsKotlin(root, _source) {
58
+ const out = [];
59
+ const list = childOfType(root, "import_list");
60
+ if (!list)
61
+ return out;
62
+ for (const h of namedChildren(list)) {
63
+ if (h.type !== "import_header")
64
+ continue;
65
+ const id = childOfType(h, "identifier");
66
+ if (!id)
67
+ continue;
68
+ const isWildcard = /\.\*\s*$/.test(h.text);
69
+ const from = id.text;
70
+ const sym = isWildcard ? "*" : (from.split(".").pop() ?? from);
71
+ out.push({ symbol: sym, from, isNamespaceImport: isWildcard });
72
+ }
73
+ return out;
74
+ }
75
+ /* ─── symbol extraction ───────────────────────────────────────────────────── */
76
+ export function extractKotlin(root, _source) {
77
+ return collect(namedChildren(root), false);
78
+ }
79
+ function collect(nodes, insideClass) {
80
+ const out = [];
81
+ for (const n of nodes) {
82
+ const res = handle(n, insideClass);
83
+ if (res)
84
+ out.push(res);
85
+ }
86
+ return out;
87
+ }
88
+ function handle(node, insideClass) {
89
+ switch (node.type) {
90
+ case "class_declaration": {
91
+ const nameNode = firstChildOfTypes(node, TYPE_NAME_NODES);
92
+ if (!nameNode)
93
+ return null;
94
+ const body = childOfType(node, "class_body") ?? childOfType(node, "enum_class_body");
95
+ return makeSymbol({
96
+ name: nameNode.text,
97
+ kind: classKind(node),
98
+ node,
99
+ rawKind: node.type,
100
+ visibility: vis(node),
101
+ exported: exported(node),
102
+ doc: leadingComment(node),
103
+ children: body ? collect(namedChildren(body), true) : [],
104
+ });
105
+ }
106
+ case "object_declaration": {
107
+ const nameNode = firstChildOfTypes(node, TYPE_NAME_NODES);
108
+ if (!nameNode)
109
+ return null;
110
+ const body = childOfType(node, "class_body");
111
+ return makeSymbol({
112
+ name: nameNode.text,
113
+ kind: "class",
114
+ node,
115
+ rawKind: node.type,
116
+ visibility: vis(node),
117
+ exported: exported(node),
118
+ doc: leadingComment(node),
119
+ children: body ? collect(namedChildren(body), true) : [],
120
+ });
121
+ }
122
+ case "function_declaration": {
123
+ const nameNode = firstChildOfTypes(node, SIMPLE_NAME_NODES);
124
+ if (!nameNode)
125
+ return null;
126
+ const body = childOfType(node, "function_body");
127
+ return makeSymbol({
128
+ name: nameNode.text,
129
+ kind: insideClass ? "method" : "function",
130
+ node,
131
+ rawKind: node.type,
132
+ signature: headerSignature(node, body),
133
+ visibility: vis(node),
134
+ exported: exported(node),
135
+ doc: leadingComment(node),
136
+ });
137
+ }
138
+ case "property_declaration": {
139
+ // variable_declaration → simple_identifier
140
+ const vd = childOfType(node, "variable_declaration");
141
+ const nameNode = vd ? firstChildOfTypes(vd, SIMPLE_NAME_NODES) : firstChildOfTypes(node, SIMPLE_NAME_NODES);
142
+ if (!nameNode)
143
+ return null;
144
+ const m = modifiersText(node);
145
+ const kind = insideClass ? "field" : (/\bconst\b/.test(m) ? "const" : "var");
146
+ return makeSymbol({
147
+ name: nameNode.text,
148
+ kind,
149
+ node,
150
+ rawKind: node.type,
151
+ signature: node.text.replace(/\s+/g, " ").trim(),
152
+ visibility: vis(node),
153
+ exported: exported(node),
154
+ });
155
+ }
156
+ default:
157
+ return null;
158
+ }
159
+ }
@@ -0,0 +1,192 @@
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
+ function modifiersText(node) {
13
+ const m = childOfType(node, "modifiers");
14
+ return m ? m.text : "";
15
+ }
16
+ function vis(node) {
17
+ const m = modifiersText(node);
18
+ if (/\b(private|fileprivate)\b/.test(m))
19
+ return "private";
20
+ return "public"; // treat internal/public/open as public
21
+ }
22
+ function exported(node) {
23
+ const m = modifiersText(node);
24
+ return /\b(public|open)\b/.test(m) || !/\b(private|fileprivate|internal)\b/.test(m);
25
+ }
26
+ /** Swift uses class_declaration for `class`, `struct`, `enum`, `actor`, `extension`. */
27
+ function classDeclKind(node) {
28
+ const head = node.text.slice(0, 80);
29
+ if (/\bstruct\b/.test(head))
30
+ return "struct";
31
+ if (/\benum\b/.test(head))
32
+ return "enum";
33
+ const body = childOfType(node, "enum_class_body");
34
+ if (body)
35
+ return "enum";
36
+ return "class";
37
+ }
38
+ /* ─── imports ─────────────────────────────────────────────────────────────── */
39
+ export function extractImportsSwift(root, _source) {
40
+ const out = [];
41
+ for (const c of namedChildren(root)) {
42
+ if (c.type !== "import_declaration")
43
+ continue;
44
+ const id = childOfType(c, "identifier");
45
+ const text = id ? id.text : c.text.replace(/^import\s+/, "").trim();
46
+ out.push({ symbol: text.split(".").pop() ?? text, from: text });
47
+ }
48
+ return out;
49
+ }
50
+ /* ─── symbol extraction ───────────────────────────────────────────────────── */
51
+ export function extractSwift(root, _source) {
52
+ return collect(namedChildren(root), false);
53
+ }
54
+ function collect(nodes, insideClass) {
55
+ const out = [];
56
+ for (const n of nodes) {
57
+ const res = handle(n, insideClass);
58
+ if (res)
59
+ out.push(res);
60
+ }
61
+ return out;
62
+ }
63
+ function handle(node, insideClass) {
64
+ switch (node.type) {
65
+ case "class_declaration": {
66
+ const name = nameOf(node);
67
+ if (!name)
68
+ return null;
69
+ const kind = classDeclKind(node);
70
+ const body = childOfType(node, "class_body") ?? childOfType(node, "enum_class_body");
71
+ return makeSymbol({
72
+ name,
73
+ kind,
74
+ node,
75
+ rawKind: node.type,
76
+ visibility: vis(node),
77
+ exported: exported(node),
78
+ doc: leadingComment(node),
79
+ children: body ? collect(namedChildren(body), true) : [],
80
+ });
81
+ }
82
+ case "protocol_declaration": {
83
+ const name = nameOf(node);
84
+ if (!name)
85
+ return null;
86
+ const body = childOfType(node, "protocol_body");
87
+ const kids = [];
88
+ if (body) {
89
+ for (const m of namedChildren(body)) {
90
+ if (m.type === "protocol_function_declaration") {
91
+ const n = nameOf(m);
92
+ if (n) {
93
+ kids.push(makeSymbol({
94
+ name: n,
95
+ kind: "method",
96
+ node: m,
97
+ rawKind: m.type,
98
+ signature: headerSignature(m, null),
99
+ visibility: "public",
100
+ exported: true,
101
+ }));
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return makeSymbol({
107
+ name,
108
+ kind: "interface",
109
+ node,
110
+ rawKind: node.type,
111
+ visibility: vis(node),
112
+ exported: exported(node),
113
+ doc: leadingComment(node),
114
+ children: kids,
115
+ });
116
+ }
117
+ case "function_declaration": {
118
+ const name = nameOf(node);
119
+ if (!name)
120
+ return null;
121
+ const body = childOfType(node, "function_body");
122
+ return makeSymbol({
123
+ name,
124
+ kind: insideClass ? "method" : "function",
125
+ node,
126
+ rawKind: node.type,
127
+ signature: headerSignature(node, body),
128
+ visibility: vis(node),
129
+ exported: exported(node),
130
+ doc: leadingComment(node),
131
+ });
132
+ }
133
+ case "init_declaration": {
134
+ return makeSymbol({
135
+ name: "init",
136
+ kind: "method",
137
+ node,
138
+ rawKind: node.type,
139
+ signature: headerSignature(node, childOfType(node, "function_body")),
140
+ visibility: vis(node),
141
+ exported: exported(node),
142
+ });
143
+ }
144
+ case "property_declaration": {
145
+ // name is `pattern` field; the pattern contains simple_identifier
146
+ const pat = node.childForFieldName("name");
147
+ let name = null;
148
+ if (pat) {
149
+ const id = pat.type === "simple_identifier" ? pat : findFirstNamed(pat, "simple_identifier");
150
+ name = id ? id.text : null;
151
+ }
152
+ if (!name)
153
+ return null;
154
+ const isLet = /\blet\b/.test(node.text.slice(0, 30));
155
+ return makeSymbol({
156
+ name,
157
+ kind: insideClass ? "field" : (isLet ? "const" : "var"),
158
+ node,
159
+ rawKind: node.type,
160
+ signature: node.text.replace(/\s+/g, " ").trim(),
161
+ visibility: vis(node),
162
+ exported: exported(node),
163
+ });
164
+ }
165
+ case "enum_entry": {
166
+ const name = nameOf(node);
167
+ if (!name)
168
+ return null;
169
+ return makeSymbol({
170
+ name,
171
+ kind: "field",
172
+ node,
173
+ rawKind: node.type,
174
+ });
175
+ }
176
+ default:
177
+ return null;
178
+ }
179
+ }
180
+ function findFirstNamed(node, type) {
181
+ for (let i = 0; i < node.namedChildCount; i++) {
182
+ const c = node.namedChild(i);
183
+ if (!c)
184
+ continue;
185
+ if (c.type === type)
186
+ return c;
187
+ const deep = findFirstNamed(c, type);
188
+ if (deep)
189
+ return deep;
190
+ }
191
+ return null;
192
+ }
package/dist/graph.js CHANGED
@@ -139,7 +139,10 @@ export function buildSymbolGraph(skeletons, root) {
139
139
  else if (skel.language === "java" ||
140
140
  skel.language === "csharp" ||
141
141
  skel.language === "rust" ||
142
- skel.language === "go") {
142
+ skel.language === "go" ||
143
+ skel.language === "kotlin" ||
144
+ skel.language === "c" ||
145
+ skel.language === "cpp") {
143
146
  wireCrossLangImport(skel, imp, fromFileAbs, root, crossIndex, exportedSymbolMap, edges);
144
147
  }
145
148
  }
package/dist/registry.js CHANGED
@@ -5,6 +5,10 @@ import { extractGo, extractImportsGo } from "./extractors/go.js";
5
5
  import { extractRust, extractImportsRust } from "./extractors/rust.js";
6
6
  import { extractJava, extractDirectivesJava, extractImportsJava } from "./extractors/java.js";
7
7
  import { extractCSharp, extractDirectivesCSharp, extractImportsCSharp } from "./extractors/csharp.js";
8
+ import { extractC, extractImportsC } from "./extractors/c.js";
9
+ import { extractCpp, extractImportsCpp } from "./extractors/cpp.js";
10
+ import { extractKotlin, extractDirectivesKotlin, extractImportsKotlin } from "./extractors/kotlin.js";
11
+ import { extractSwift, extractImportsSwift } from "./extractors/swift.js";
8
12
  const TS_ENTRY = (language, grammar) => ({
9
13
  language,
10
14
  grammar,
@@ -26,19 +30,30 @@ const BY_EXT = {
26
30
  ".go": { language: "go", grammar: "go", extract: extractGo, extractImports: extractImportsGo },
27
31
  ".rs": { language: "rust", grammar: "rust", extract: extractRust, extractImports: extractImportsRust },
28
32
  ".java": {
29
- language: "java",
30
- grammar: "java",
31
- extract: extractJava,
32
- extractDirectives: extractDirectivesJava,
33
- extractImports: extractImportsJava,
33
+ language: "java", grammar: "java",
34
+ extract: extractJava, extractDirectives: extractDirectivesJava, extractImports: extractImportsJava,
34
35
  },
35
36
  ".cs": {
36
- language: "csharp",
37
- grammar: "c_sharp",
38
- extract: extractCSharp,
39
- extractDirectives: extractDirectivesCSharp,
40
- extractImports: extractImportsCSharp,
37
+ language: "csharp", grammar: "c_sharp",
38
+ extract: extractCSharp, extractDirectives: extractDirectivesCSharp, extractImports: extractImportsCSharp,
41
39
  },
40
+ ".c": { language: "c", grammar: "c", extract: extractC, extractImports: extractImportsC },
41
+ ".h": { language: "c", grammar: "c", extract: extractC, extractImports: extractImportsC },
42
+ ".cpp": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
43
+ ".cxx": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
44
+ ".cc": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
45
+ ".hpp": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
46
+ ".hxx": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
47
+ ".hh": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
48
+ ".kt": {
49
+ language: "kotlin", grammar: "kotlin",
50
+ extract: extractKotlin, extractDirectives: extractDirectivesKotlin, extractImports: extractImportsKotlin,
51
+ },
52
+ ".kts": {
53
+ language: "kotlin", grammar: "kotlin",
54
+ extract: extractKotlin, extractDirectives: extractDirectivesKotlin, extractImports: extractImportsKotlin,
55
+ },
56
+ ".swift": { language: "swift", grammar: "swift", extract: extractSwift, extractImports: extractImportsSwift },
42
57
  };
43
58
  export function detectLanguage(filePath) {
44
59
  return BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
package/dist/resolver.js CHANGED
@@ -81,7 +81,7 @@ export async function getOrBuildCrossLangIndex(root) {
81
81
  const ext = path.extname(abs).toLowerCase();
82
82
  // Only Java/C# contribute to the index (Rust resolves via direct
83
83
  // module-path walk against the filesystem, no index needed).
84
- if (ext !== ".java" && ext !== ".cs")
84
+ if (ext !== ".java" && ext !== ".cs" && ext !== ".kt" && ext !== ".kts")
85
85
  continue;
86
86
  const rel = path.relative(key, abs).split(path.sep).join("/");
87
87
  try {
@@ -168,7 +168,7 @@ function assembleResolved(imp, resolvedAbs, resolvedRel, isExternal, enrichment)
168
168
  return out;
169
169
  }
170
170
  /* ─── Public entry point ──────────────────────────────────────────────────── */
171
- const CROSS_LANG = new Set(["java", "csharp", "rust", "go"]);
171
+ const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp"]);
172
172
  export async function resolveFileImports(skel, absPath, root) {
173
173
  if (!skel.imports || skel.imports.length === 0)
174
174
  return [];
package/dist/skeleton.js CHANGED
@@ -48,10 +48,15 @@ export async function buildSkeleton(absPath, relPath, opts) {
48
48
  if (stat.size > opts.maxFileBytes) {
49
49
  throw new Error(`File is ${stat.size} bytes, exceeds maxFileBytes (${opts.maxFileBytes}). Increase the limit to parse it.`);
50
50
  }
51
- // Return cached result if file hasn't changed
51
+ // Return cached result if file hasn't changed. The cached SkeletonFile's
52
+ // `.file` is whatever relPath the first caller used; the same absolute file
53
+ // can be requested under a different root (different relPath), so override
54
+ // `.file` per call to avoid leaking a stale rel path into callers/indexes.
52
55
  const cached = getCached(absPath, opts.detail);
53
- if (cached)
54
- return cached;
56
+ if (cached) {
57
+ const wantFile = relPath.split(path.sep).join("/");
58
+ return cached.file === wantFile ? cached : { ...cached, file: wantFile };
59
+ }
55
60
  const source = fs.readFileSync(absPath, "utf8");
56
61
  const root = await parseSource(entry.grammar, source);
57
62
  let symbols = entry.extract(root, source);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server that maps source files into a normalized code skeleton (JSON + HTML) using tree-sitter.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",