universal-ast-mapper 0.8.0 → 0.8.2
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 +10 -6
- package/dist/callgraph.js +24 -2
- package/dist/crosslang.js +115 -2
- package/dist/graph.js +5 -1
- package/dist/resolver.js +2 -2
- package/dist/skeleton.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,12 @@ Built on [tree-sitter](https://tree-sitter.github.io/) WASM grammars. Zero regex
|
|
|
10
10
|
|--------------------------|:-----:|:------:|:---:|:----:|:----:|:---:|:---:|:---:|:---:|:-----:|
|
|
11
11
|
| Symbol extraction | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
12
12
|
| Imports parsing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
13
|
-
| Graph `imports` edges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
14
|
-
| `resolve_imports` enrich | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
15
|
-
| Call graph callee origin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
|
|
16
|
-
| Reverse `calledBy` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
|
|
13
|
+
| Graph `imports` edges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
14
|
+
| `resolve_imports` enrich | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
15
|
+
| Call graph callee origin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | ✅ | — |
|
|
16
|
+
| Reverse `calledBy` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | ✅ | — |
|
|
17
17
|
|
|
18
|
-
> v0.8.0
|
|
18
|
+
> As of v0.8.2, all four v0.8.0 languages have **cross-file graph + resolver** wiring: Kotlin (FQCN/package index), C/C++ (`#include` with header↔impl pairing), and Swift (module = directory under `Sources/`). Call-graph callee origin is resolved for Kotlin; for C/C++/Swift it stays limited because their imports don't name individual symbols. (Ruby grammar in `tree-sitter-wasms@0.1.13` is unstable and was skipped.)
|
|
19
19
|
|
|
20
20
|
Each language uses the resolution strategy that fits it:
|
|
21
21
|
- **TS/JS/Python** — relative paths (`./foo`, `..mod`) resolved against the importing file's directory, with TS-ESM `.js` → `.ts` rewriting.
|
|
@@ -23,6 +23,8 @@ Each language uses the resolution strategy that fits it:
|
|
|
23
23
|
- **Rust** — `Cargo.toml` ancestor → `crate::` / `self::` / `super::` walks; supports `mod.rs` + Rust-2018 sibling-dir style.
|
|
24
24
|
- **Java** — project-wide FQCN index (`package + "." + className → file`) built lazily on first cross-lang call; supports wildcard imports.
|
|
25
25
|
- **C#** — namespace-to-files index plus a `<ns>.<TypeName>` index so `using App.Models` + `new Inventory()` resolves to the right file.
|
|
26
|
+
- **Kotlin** — project-wide FQCN index (`package + "." + ClassName → file`), like Java; wildcard `import pkg.*` pulls every file in the package.
|
|
27
|
+
- **C / C++** — `#include "..."` resolved against the including file's directory; headers auto-paired with same-name `.c`/`.cpp`/`.cc`/`.cxx` impl files. `<system>` includes stay external.
|
|
26
28
|
|
|
27
29
|
For C# and Go (where imports don't name the called symbol), reverse `calledBy` falls back to **call-site scanning** of candidate files.
|
|
28
30
|
|
|
@@ -290,7 +292,7 @@ Parse a function body → extract every call expression, resolve callees via the
|
|
|
290
292
|
```
|
|
291
293
|
|
|
292
294
|
Supports all 8 languages with per-language call extraction (TS/JS `member_expression`, Rust `field_expression`/`scoped_identifier`, Java `method_invocation`, C# `invocation_expression`, etc.) and constructor calls (`new Foo`).
|
|
293
|
-
Handles TS/JS destructured aliases (`const { sign } = jwt`), Java FQCN imports, C# `using` namespaces (via project-wide type index), Rust `use crate::path::Item`, Go `pkg.Func` (via go.mod module path). Reverse `calledBy` uses call-site scanning for C# and Go where import statements don't name the called symbol.
|
|
295
|
+
Handles TS/JS destructured aliases (`const { sign } = jwt`), Java FQCN imports, C# `using` namespaces (via project-wide type index), Rust `use crate::path::Item`, Go `pkg.Func` (via go.mod module path), Kotlin FQCN/package imports, C/C++ `#include`, and Swift module imports (`import <Module>` → files under `Sources/<Module>/`). Reverse `calledBy` uses call-site scanning for C# and Go where import statements don't name the called symbol.
|
|
294
296
|
|
|
295
297
|
**Params:** `path`, `function`, `scanDir`
|
|
296
298
|
|
|
@@ -480,6 +482,8 @@ src/
|
|
|
480
482
|
|
|
481
483
|
| Version | What changed |
|
|
482
484
|
|---------|--------------|
|
|
485
|
+
| **0.8.2** | **Swift cross-file wiring** — `import <Module>` resolves to that module's files (module = the `Sources/<Module>/` directory, else parent dir), wired into `build_symbol_graph` + `resolve_imports`. System modules (Foundation, UIKit, …) stay external. Completes cross-file graph/resolver support for all four v0.8.0 languages. |
|
|
486
|
+
| **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`). |
|
|
483
487
|
| **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. |
|
|
484
488
|
| **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) |
|
|
485
489
|
| **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 |
|
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", "swift"]);
|
|
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
|
-
|
|
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,9 @@ 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(),
|
|
29
|
+
swiftModules: new Map(),
|
|
27
30
|
};
|
|
28
31
|
for (const skel of skeletons) {
|
|
29
32
|
if (skel.language === "java") {
|
|
@@ -55,9 +58,43 @@ export function buildCrossLangIndex(skeletons) {
|
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
61
|
+
else if (skel.language === "kotlin") {
|
|
62
|
+
const pkg = getDirectiveValue(skel, "package:");
|
|
63
|
+
if (!pkg)
|
|
64
|
+
continue;
|
|
65
|
+
const pkgFiles = index.kotlinPackages.get(pkg) ?? [];
|
|
66
|
+
pkgFiles.push(skel.file);
|
|
67
|
+
index.kotlinPackages.set(pkg, pkgFiles);
|
|
68
|
+
for (const sym of topTypeSymbols(skel.symbols)) {
|
|
69
|
+
index.kotlinFqcn.set(`${pkg}.${sym.name}`, skel.file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (skel.language === "swift") {
|
|
73
|
+
const mod = swiftModuleOf(skel.file);
|
|
74
|
+
if (!mod)
|
|
75
|
+
continue;
|
|
76
|
+
const arr = index.swiftModules.get(mod) ?? [];
|
|
77
|
+
arr.push(skel.file);
|
|
78
|
+
index.swiftModules.set(mod, arr);
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
return index;
|
|
60
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Derive a Swift module name from a project-relative file path.
|
|
85
|
+
* SwiftPM convention: sources live under `Sources/<ModuleName>/...`, so the
|
|
86
|
+
* module is the path segment right after `Sources`. For flat layouts we fall
|
|
87
|
+
* back to the immediate parent directory name. Returns null for bare files.
|
|
88
|
+
*/
|
|
89
|
+
function swiftModuleOf(relFile) {
|
|
90
|
+
const parts = relFile.split("/");
|
|
91
|
+
const srcIdx = parts.lastIndexOf("Sources");
|
|
92
|
+
if (srcIdx >= 0 && srcIdx + 1 < parts.length - 1)
|
|
93
|
+
return parts[srcIdx + 1];
|
|
94
|
+
if (parts.length >= 2)
|
|
95
|
+
return parts[parts.length - 2];
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
61
98
|
/* ─── Rust module resolution ──────────────────────────────────────────────── */
|
|
62
99
|
function findCargoRoot(fromAbs, projectRoot) {
|
|
63
100
|
let dir = path.dirname(fromAbs);
|
|
@@ -262,6 +299,47 @@ export function resolveGoImport(importFrom, fromAbs, projectRoot) {
|
|
|
262
299
|
export function clearGoModuleCache() {
|
|
263
300
|
goModuleCache.clear();
|
|
264
301
|
}
|
|
302
|
+
/* ─── C / C++ #include resolution ─────────────────────────────────────────── */
|
|
303
|
+
const HEADER_EXTS = [".h", ".hpp", ".hxx", ".hh"];
|
|
304
|
+
const IMPL_EXTS = [".c", ".cpp", ".cc", ".cxx"];
|
|
305
|
+
/**
|
|
306
|
+
* Resolve a C/C++ `#include "foo.h"` to in-project files.
|
|
307
|
+
* Convention: also pair foo.h with foo.c/.cpp in the same directory so the
|
|
308
|
+
* graph captures the header → impl relationship.
|
|
309
|
+
* `#include <foo.h>` (system headers) returns null (external).
|
|
310
|
+
*/
|
|
311
|
+
export function resolveCInclude(importFrom, fromAbs, projectRoot) {
|
|
312
|
+
// System headers like stdio.h, vector, etc. — leave to external.
|
|
313
|
+
const isSystemHeader = !importFrom.includes("/") && !importFrom.includes(".") ||
|
|
314
|
+
/^(stdio|stdlib|string|vector|memory|cstdint|cstdlib|cstring|iostream)/.test(importFrom);
|
|
315
|
+
// We only check the actual filesystem; if a system header happens to exist
|
|
316
|
+
// locally we still link it, otherwise it falls through to null.
|
|
317
|
+
const fromDir = path.dirname(fromAbs);
|
|
318
|
+
const headerAbs = path.resolve(fromDir, importFrom);
|
|
319
|
+
const out = [];
|
|
320
|
+
if (existsFile(headerAbs)) {
|
|
321
|
+
const rel = path.relative(projectRoot, headerAbs).split(path.sep).join("/");
|
|
322
|
+
// Reject paths that escape the project root.
|
|
323
|
+
if (!rel.startsWith(".."))
|
|
324
|
+
out.push(rel);
|
|
325
|
+
// Pair foo.h with foo.{c,cpp,cc,cxx} in the same directory.
|
|
326
|
+
const ext = path.extname(headerAbs).toLowerCase();
|
|
327
|
+
if (HEADER_EXTS.includes(ext)) {
|
|
328
|
+
const base = headerAbs.slice(0, -ext.length);
|
|
329
|
+
for (const implExt of IMPL_EXTS) {
|
|
330
|
+
const implAbs = base + implExt;
|
|
331
|
+
if (existsFile(implAbs)) {
|
|
332
|
+
const implRel = path.relative(projectRoot, implAbs).split(path.sep).join("/");
|
|
333
|
+
if (!implRel.startsWith(".."))
|
|
334
|
+
out.push(implRel);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (isSystemHeader && out.length === 0)
|
|
340
|
+
return null;
|
|
341
|
+
return out.length > 0 ? out : null;
|
|
342
|
+
}
|
|
265
343
|
/**
|
|
266
344
|
* Resolve an ImportRef in a non-relative-path language to a graph target.
|
|
267
345
|
* Returns null for unresolvable / external imports.
|
|
@@ -301,12 +379,47 @@ export function resolveCrossLangTarget(imp, skel, fromAbs, projectRoot, index) {
|
|
|
301
379
|
const files = resolveGoImport(imp.from, fromAbs, projectRoot);
|
|
302
380
|
if (!files || files.length === 0)
|
|
303
381
|
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).
|
|
306
382
|
const filtered = files.filter((f) => f !== skel.file);
|
|
307
383
|
if (filtered.length === 0)
|
|
308
384
|
return null;
|
|
309
385
|
return { kind: "file", files: filtered };
|
|
310
386
|
}
|
|
387
|
+
if (skel.language === "kotlin") {
|
|
388
|
+
if (imp.symbol === "*") {
|
|
389
|
+
const files = index.kotlinPackages.get(imp.from);
|
|
390
|
+
if (files && files.length > 0) {
|
|
391
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
392
|
+
if (filtered.length > 0)
|
|
393
|
+
return { kind: "file", files: filtered };
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const targetFile = index.kotlinFqcn.get(imp.from);
|
|
398
|
+
if (targetFile && targetFile !== skel.file) {
|
|
399
|
+
return { kind: "symbol", file: targetFile, symbol: imp.symbol };
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
if (skel.language === "c" || skel.language === "cpp") {
|
|
404
|
+
const files = resolveCInclude(imp.from, fromAbs, projectRoot);
|
|
405
|
+
if (!files || files.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
408
|
+
if (filtered.length === 0)
|
|
409
|
+
return null;
|
|
410
|
+
return { kind: "file", files: filtered };
|
|
411
|
+
}
|
|
412
|
+
if (skel.language === "swift") {
|
|
413
|
+
// `import <Module>` brings in another in-project module's public symbols
|
|
414
|
+
// (no symbol named). Resolve to that module's files; unknown modules
|
|
415
|
+
// (Foundation, UIKit, …) are external.
|
|
416
|
+
const files = index.swiftModules.get(imp.from);
|
|
417
|
+
if (files && files.length > 0) {
|
|
418
|
+
const filtered = files.filter((f) => f !== skel.file);
|
|
419
|
+
if (filtered.length > 0)
|
|
420
|
+
return { kind: "file", files: filtered };
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
311
424
|
return null;
|
|
312
425
|
}
|
package/dist/graph.js
CHANGED
|
@@ -139,7 +139,11 @@ 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" ||
|
|
146
|
+
skel.language === "swift") {
|
|
143
147
|
wireCrossLangImport(skel, imp, fromFileAbs, root, crossIndex, exportedSymbolMap, edges);
|
|
144
148
|
}
|
|
145
149
|
}
|
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" && ext !== ".swift")
|
|
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", "swift"]);
|
|
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
|
-
|
|
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