universal-ast-mapper 0.5.3 → 0.8.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 +34 -6
- package/dist/callgraph.js +266 -86
- package/dist/crosslang.js +312 -0
- package/dist/extractors/c.js +204 -0
- package/dist/extractors/cpp.js +272 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/kotlin.js +159 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/extractors/swift.js +192 -0
- package/dist/graph.js +77 -38
- package/dist/registry.js +33 -1
- package/dist/resolver.js +117 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,27 @@ 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
|
|
7
|
+
**Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift
|
|
8
|
+
|
|
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.)
|
|
19
|
+
|
|
20
|
+
Each language uses the resolution strategy that fits it:
|
|
21
|
+
- **TS/JS/Python** — relative paths (`./foo`, `..mod`) resolved against the importing file's directory, with TS-ESM `.js` → `.ts` rewriting.
|
|
22
|
+
- **Go** — `go.mod` ancestor lookup → module path prefix → package directory → all `.go` files (skips `_test.go`).
|
|
23
|
+
- **Rust** — `Cargo.toml` ancestor → `crate::` / `self::` / `super::` walks; supports `mod.rs` + Rust-2018 sibling-dir style.
|
|
24
|
+
- **Java** — project-wide FQCN index (`package + "." + className → file`) built lazily on first cross-lang call; supports wildcard imports.
|
|
25
|
+
- **C#** — namespace-to-files index plus a `<ns>.<TypeName>` index so `using App.Models` + `new Inventory()` resolves to the right file.
|
|
26
|
+
|
|
27
|
+
For C# and Go (where imports don't name the called symbol), reverse `calledBy` falls back to **call-site scanning** of candidate files.
|
|
8
28
|
|
|
9
29
|
---
|
|
10
30
|
|
|
@@ -269,8 +289,8 @@ Parse a function body → extract every call expression, resolve callees via the
|
|
|
269
289
|
}
|
|
270
290
|
```
|
|
271
291
|
|
|
272
|
-
Supports
|
|
273
|
-
Handles destructured aliases
|
|
292
|
+
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.
|
|
274
294
|
|
|
275
295
|
**Params:** `path`, `function`, `scanDir`
|
|
276
296
|
|
|
@@ -435,8 +455,9 @@ src/
|
|
|
435
455
|
├── registry.ts — language detection + extractor registry
|
|
436
456
|
├── parser.ts — tree-sitter WASM loader + AST node helpers
|
|
437
457
|
├── skeleton.ts — buildSkeleton(), collectSourceFiles() + parse cache
|
|
438
|
-
├── resolver.ts — resolveImportPath(), resolveFileImports()
|
|
439
|
-
├──
|
|
458
|
+
├── resolver.ts — resolveImportPath(), resolveFileImports() (TS/JS/Python relative)
|
|
459
|
+
├── crosslang.ts — Java FQCN / C# namespace / Rust crate / Go module resolvers + index cache
|
|
460
|
+
├── graph.ts — buildSymbolGraph() (language-aware second pass)
|
|
440
461
|
├── graph-analysis.ts — findDeadExports(), findCircularDeps(), getChangeImpact(),
|
|
441
462
|
│ getFileDeps(), getTopSymbols()
|
|
442
463
|
├── callgraph.ts — buildCallGraph() — AST-level call extraction
|
|
@@ -447,7 +468,10 @@ src/
|
|
|
447
468
|
├── common.ts — makeSymbol(), toOutline()
|
|
448
469
|
├── typescript.ts — TS/JS/TSX: symbols + imports + re-exports
|
|
449
470
|
├── python.ts — Python: symbols + relative import resolution
|
|
450
|
-
|
|
471
|
+
├── go.ts — Go: symbols + imports
|
|
472
|
+
├── rust.ts — Rust: struct/trait/enum/impl + `use` imports
|
|
473
|
+
├── java.ts — Java: class/interface/enum/method/field + package + imports
|
|
474
|
+
└── csharp.ts — C#: namespace recursion + class/struct/interface/property + `using`
|
|
451
475
|
```
|
|
452
476
|
|
|
453
477
|
---
|
|
@@ -456,6 +480,10 @@ src/
|
|
|
456
480
|
|
|
457
481
|
| Version | What changed |
|
|
458
482
|
|---------|--------------|
|
|
483
|
+
| **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
|
+
| **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
|
+
| **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 |
|
|
486
|
+
| **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) |
|
|
459
487
|
| **0.5.2** | Iterative DFS in `findCircularDeps` (eliminates stack overflow on large codebases) · `build_symbol_graph` inline size guard (>2000 nodes → stats + warning) · integration test suite (`test/analysis.mjs`) |
|
|
460
488
|
| **0.5.1** | Re-export tracking (`export { X } from './foo'`, barrel files) · `export const` surfaced as symbols · `const X = class {}` support · Python relative import fix · parser instance cache |
|
|
461
489
|
| **0.5.0** | Call graph destructuring aliases · in-process parse cache · `.ast-map.config.json` · general validation rules (large-file, too-many-imports, god-export) |
|
package/dist/callgraph.js
CHANGED
|
@@ -4,28 +4,89 @@ import { parseSource } from "./parser.js";
|
|
|
4
4
|
import { buildSkeleton } from "./skeleton.js";
|
|
5
5
|
import { resolveOptions, loadProjectConfig } from "./config.js";
|
|
6
6
|
import { detectLanguage } from "./registry.js";
|
|
7
|
-
import { resolveImportPath } from "./resolver.js";
|
|
8
|
-
|
|
7
|
+
import { resolveImportPath, getOrBuildCrossLangIndex } from "./resolver.js";
|
|
8
|
+
import { resolveCrossLangTarget } from "./crosslang.js";
|
|
9
|
+
const CROSS_LANG = new Set(["java", "csharp", "rust", "go"]);
|
|
10
|
+
function pushCall(out, callee, anchor) {
|
|
11
|
+
if (callee && anchor)
|
|
12
|
+
out.push({ callee, line: anchor.startPosition.row + 1 });
|
|
13
|
+
}
|
|
9
14
|
function collectCalls(node, out) {
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const t = node.type;
|
|
16
|
+
// ── call_expression: TS/JS (member_expression) | Python "call" (attribute) |
|
|
17
|
+
// Go (selector_expression) | Rust (field_expression, scoped_identifier)
|
|
18
|
+
if (t === "call_expression" || t === "call") {
|
|
12
19
|
const fn = node.childForFieldName("function");
|
|
13
20
|
if (fn) {
|
|
14
21
|
let callee = null;
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
switch (fn.type) {
|
|
23
|
+
case "identifier":
|
|
24
|
+
callee = fn.text;
|
|
25
|
+
break;
|
|
26
|
+
case "member_expression":
|
|
27
|
+
case "attribute": {
|
|
28
|
+
const obj = fn.childForFieldName("object");
|
|
29
|
+
const prop = fn.childForFieldName("property") ?? fn.childForFieldName("attribute");
|
|
30
|
+
if (prop)
|
|
31
|
+
callee = obj ? `${obj.text}.${prop.text}` : prop.text;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "field_expression": {
|
|
35
|
+
// Rust: inv.reserve — fields are `value` and `field`
|
|
36
|
+
const obj = fn.childForFieldName("value");
|
|
37
|
+
const fld = fn.childForFieldName("field");
|
|
38
|
+
if (fld)
|
|
39
|
+
callee = obj ? `${obj.text}.${fld.text}` : fld.text;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "scoped_identifier":
|
|
43
|
+
// Rust: String::from / helpers::format — keep full path
|
|
44
|
+
callee = fn.text;
|
|
45
|
+
break;
|
|
46
|
+
case "selector_expression": {
|
|
47
|
+
// Go: pkg.Func
|
|
48
|
+
const obj = fn.childForFieldName("operand");
|
|
49
|
+
const fld = fn.childForFieldName("field");
|
|
50
|
+
if (fld)
|
|
51
|
+
callee = obj ? `${obj.text}.${fld.text}` : fld.text;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
17
54
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
pushCall(out, callee, fn);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ── Java method invocation
|
|
59
|
+
else if (t === "method_invocation") {
|
|
60
|
+
const name = node.childForFieldName("name");
|
|
61
|
+
const obj = node.childForFieldName("object");
|
|
62
|
+
if (name)
|
|
63
|
+
pushCall(out, obj ? `${obj.text}.${name.text}` : name.text, name);
|
|
64
|
+
}
|
|
65
|
+
// ── C# invocation expression
|
|
66
|
+
else if (t === "invocation_expression") {
|
|
67
|
+
const fn = node.childForFieldName("function");
|
|
68
|
+
if (fn)
|
|
69
|
+
pushCall(out, fn.text, fn);
|
|
70
|
+
}
|
|
71
|
+
// ── Java + C# constructor call: new Foo(...)
|
|
72
|
+
else if (t === "object_creation_expression") {
|
|
73
|
+
let typeNode = node.childForFieldName("type");
|
|
74
|
+
if (!typeNode) {
|
|
75
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
76
|
+
const c = node.namedChild(i);
|
|
77
|
+
if (c &&
|
|
78
|
+
(c.type === "identifier" ||
|
|
79
|
+
c.type === "type_identifier" ||
|
|
80
|
+
c.type === "scoped_identifier" ||
|
|
81
|
+
c.type === "qualified_name" ||
|
|
82
|
+
c.type === "generic_type")) {
|
|
83
|
+
typeNode = c;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
25
86
|
}
|
|
26
|
-
if (callee)
|
|
27
|
-
out.push({ callee, line: fn.startPosition.row + 1 });
|
|
28
87
|
}
|
|
88
|
+
if (typeNode)
|
|
89
|
+
pushCall(out, `new ${typeNode.text}`, typeNode);
|
|
29
90
|
}
|
|
30
91
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
31
92
|
const c = node.namedChild(i);
|
|
@@ -33,33 +94,27 @@ function collectCalls(node, out) {
|
|
|
33
94
|
collectCalls(c, out);
|
|
34
95
|
}
|
|
35
96
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
97
|
+
// ─── Function-node finder ─────────────────────────────────────────────────────
|
|
98
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
99
|
+
"function_declaration", // TS / JS / Go
|
|
100
|
+
"generator_function_declaration",
|
|
101
|
+
"method_definition", // TS / JS class member
|
|
102
|
+
"method_signature",
|
|
103
|
+
"abstract_method_signature",
|
|
104
|
+
"function_definition", // Python
|
|
105
|
+
"async_function_definition", // Python async
|
|
106
|
+
"method_declaration", // Go / Java / C#
|
|
107
|
+
"constructor_declaration", // Java / C#
|
|
108
|
+
"function_item", // Rust
|
|
109
|
+
]);
|
|
45
110
|
function findFunctionNode(root, name) {
|
|
46
111
|
function walk(node) {
|
|
47
|
-
|
|
48
|
-
// Direct named functions / methods
|
|
49
|
-
if (t === "function_declaration" ||
|
|
50
|
-
t === "generator_function_declaration" ||
|
|
51
|
-
t === "method_definition" ||
|
|
52
|
-
t === "method_signature" ||
|
|
53
|
-
t === "abstract_method_signature" ||
|
|
54
|
-
t === "function_definition" || // Python
|
|
55
|
-
t === "async_function_definition" || // Python async
|
|
56
|
-
t === "method_declaration" // Go
|
|
57
|
-
) {
|
|
112
|
+
if (FUNCTION_NODE_TYPES.has(node.type)) {
|
|
58
113
|
if (node.childForFieldName("name")?.text === name)
|
|
59
114
|
return node;
|
|
60
115
|
}
|
|
61
|
-
// const foo = () => ...
|
|
62
|
-
if (
|
|
116
|
+
// const foo = () => ... | const foo = function() ...
|
|
117
|
+
if (node.type === "variable_declarator") {
|
|
63
118
|
const declName = node.childForFieldName("name")?.text;
|
|
64
119
|
const value = node.childForFieldName("value");
|
|
65
120
|
if (declName === name &&
|
|
@@ -70,7 +125,6 @@ function findFunctionNode(root, name) {
|
|
|
70
125
|
return value;
|
|
71
126
|
}
|
|
72
127
|
}
|
|
73
|
-
// Recurse
|
|
74
128
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
75
129
|
const c = node.namedChild(i);
|
|
76
130
|
if (c) {
|
|
@@ -83,16 +137,7 @@ function findFunctionNode(root, name) {
|
|
|
83
137
|
}
|
|
84
138
|
return walk(root);
|
|
85
139
|
}
|
|
86
|
-
// ─── Destructuring alias tracker
|
|
87
|
-
/**
|
|
88
|
-
* Walk a subtree and collect variable destructuring patterns where the source
|
|
89
|
-
* is a known import. Handles:
|
|
90
|
-
* const { sign, verify } = jwt; → sign/verify → (jwt's source)
|
|
91
|
-
* const { readFile: rf } = fs; → rf → (fs's source)
|
|
92
|
-
* let { a, b } = someNamespace.nested; → a/b → (someNamespace's source)
|
|
93
|
-
*
|
|
94
|
-
* Returns a map of localAlias → moduleSpecifier (same format as importMap).
|
|
95
|
-
*/
|
|
140
|
+
// ─── Destructuring alias tracker (TS/JS only) ─────────────────────────────────
|
|
96
141
|
function collectDestructuredAliases(node, importMap) {
|
|
97
142
|
const aliases = new Map();
|
|
98
143
|
function walk(n) {
|
|
@@ -100,20 +145,18 @@ function collectDestructuredAliases(node, importMap) {
|
|
|
100
145
|
const nameNode = n.childForFieldName("name");
|
|
101
146
|
const valueNode = n.childForFieldName("value");
|
|
102
147
|
if (nameNode && valueNode && nameNode.type === "object_pattern") {
|
|
103
|
-
// value might be `jwt` or `jwt.utils` — base is the first identifier
|
|
104
148
|
const baseName = valueNode.text.split(".")[0];
|
|
105
|
-
const
|
|
149
|
+
const originRef = importMap.get(baseName);
|
|
150
|
+
const origin = originRef?.from ?? aliases.get(baseName);
|
|
106
151
|
if (origin) {
|
|
107
152
|
for (let i = 0; i < nameNode.namedChildCount; i++) {
|
|
108
153
|
const prop = nameNode.namedChild(i);
|
|
109
154
|
if (!prop)
|
|
110
155
|
continue;
|
|
111
|
-
// { sign } — shorthand
|
|
112
156
|
if (prop.type === "shorthand_property_identifier_pattern" ||
|
|
113
157
|
prop.type === "shorthand_property_identifier") {
|
|
114
158
|
aliases.set(prop.text, origin);
|
|
115
159
|
}
|
|
116
|
-
// { readFile: rf } — renamed
|
|
117
160
|
if (prop.type === "pair_pattern") {
|
|
118
161
|
const val = prop.childForFieldName("value");
|
|
119
162
|
if (val)
|
|
@@ -132,18 +175,52 @@ function collectDestructuredAliases(node, importMap) {
|
|
|
132
175
|
walk(node);
|
|
133
176
|
return aliases;
|
|
134
177
|
}
|
|
135
|
-
// ───
|
|
178
|
+
// ─── Base identifier of a callee expression ───────────────────────────────────
|
|
179
|
+
/** Take the leftmost identifier from "obj.method" / "Pkg::func" / "new Foo". */
|
|
180
|
+
function baseNameOf(callee) {
|
|
181
|
+
let s = callee;
|
|
182
|
+
if (s.startsWith("new "))
|
|
183
|
+
s = s.slice(4);
|
|
184
|
+
return s.split(/::|\./)[0];
|
|
185
|
+
}
|
|
186
|
+
// ─── Cross-language calledBy scan helper ──────────────────────────────────────
|
|
187
|
+
/** Last segment of a member-style callee — "Helper.fmt" -> "fmt", "compute" -> null. */
|
|
188
|
+
function memberOf(callee) {
|
|
189
|
+
const noNew = callee.startsWith("new ") ? callee.slice(4) : callee;
|
|
190
|
+
const parts = noNew.split(/::|\./);
|
|
191
|
+
return parts.length > 1 ? parts[parts.length - 1] : null;
|
|
192
|
+
}
|
|
136
193
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* @param allSkeletons Optional: pre-parsed skeletons of the whole project,
|
|
143
|
-
* used to find which files import (and thus call) this function.
|
|
144
|
-
*
|
|
145
|
-
* Returns null if the language is unsupported or the function is not found.
|
|
194
|
+
* Open a file, parse it, and check whether any call expression references
|
|
195
|
+
* `funcName` — either as a bare call `funcName(...)` or as the trailing
|
|
196
|
+
* member of a qualified call `X.funcName(...)` / `X::funcName(...)`.
|
|
197
|
+
* Used for C# / Go reverse calledBy where namespace/package imports do not
|
|
198
|
+
* name the called symbol.
|
|
146
199
|
*/
|
|
200
|
+
async function fileCallsSymbol(fileAbs, funcName) {
|
|
201
|
+
const lang = detectLanguage(fileAbs);
|
|
202
|
+
if (!lang)
|
|
203
|
+
return false;
|
|
204
|
+
let src;
|
|
205
|
+
try {
|
|
206
|
+
src = fs.readFileSync(fileAbs, "utf8");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
const root = await parseSource(lang.grammar, src);
|
|
212
|
+
const calls = [];
|
|
213
|
+
collectCalls(root, calls);
|
|
214
|
+
for (const c of calls) {
|
|
215
|
+
if (c.callee === funcName)
|
|
216
|
+
return true;
|
|
217
|
+
const m = memberOf(c.callee);
|
|
218
|
+
if (m === funcName)
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
147
224
|
export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
|
|
148
225
|
const langEntry = detectLanguage(filePath);
|
|
149
226
|
if (!langEntry)
|
|
@@ -154,25 +231,23 @@ export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
|
|
|
154
231
|
const funcNode = findFunctionNode(rootNode, funcName);
|
|
155
232
|
if (!funcNode)
|
|
156
233
|
return null;
|
|
157
|
-
// Use the body subtree for call extraction (avoids counting the signature itself)
|
|
158
234
|
const body = funcNode.childForFieldName("body") ?? funcNode;
|
|
159
235
|
const rawCalls = [];
|
|
160
236
|
collectCalls(body, rawCalls);
|
|
161
|
-
// Parse the file's imports to resolve callee origins
|
|
162
237
|
const opts = resolveOptions({ detail: "outline", emitHtml: false }, loadProjectConfig(root));
|
|
163
238
|
const skel = await buildSkeleton(filePath, relPath, opts);
|
|
164
|
-
// localName
|
|
239
|
+
// localName -> full ImportRef (so cross-lang resolution has the flags it needs)
|
|
165
240
|
const importMap = new Map();
|
|
166
241
|
for (const imp of skel.imports ?? []) {
|
|
167
242
|
if (imp.symbol !== "*" && !imp.isSideEffect) {
|
|
168
|
-
importMap.set(imp.alias ?? imp.symbol, imp
|
|
243
|
+
importMap.set(imp.alias ?? imp.symbol, imp);
|
|
169
244
|
}
|
|
170
245
|
}
|
|
171
246
|
const localNames = new Set(skel.symbols.map((s) => s.name));
|
|
172
|
-
// Track destructured aliases within the function body
|
|
173
|
-
// e.g. const { sign } = jwt → sign maps to the same source as jwt
|
|
174
247
|
const destructuredAliases = collectDestructuredAliases(body, importMap);
|
|
175
|
-
//
|
|
248
|
+
// Build cross-lang index lazily — needed for Java/C#/Rust dispatch.
|
|
249
|
+
const isCrossLang = CROSS_LANG.has(skel.language);
|
|
250
|
+
const crossIndex = isCrossLang ? await getOrBuildCrossLangIndex(root) : null;
|
|
176
251
|
const calls = [];
|
|
177
252
|
const seen = new Set();
|
|
178
253
|
for (const { callee, line } of rawCalls) {
|
|
@@ -180,48 +255,153 @@ export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
|
|
|
180
255
|
if (seen.has(key))
|
|
181
256
|
continue;
|
|
182
257
|
seen.add(key);
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
const
|
|
258
|
+
const base = baseNameOf(callee);
|
|
259
|
+
const importRef = importMap.get(base);
|
|
260
|
+
const aliasOrigin = destructuredAliases.get(base);
|
|
186
261
|
const call = { callee, line };
|
|
187
|
-
if (
|
|
188
|
-
if (
|
|
189
|
-
const
|
|
262
|
+
if (importRef) {
|
|
263
|
+
if (isCrossLang && crossIndex) {
|
|
264
|
+
const target = resolveCrossLangTarget(importRef, skel, filePath, root, crossIndex);
|
|
265
|
+
if (target) {
|
|
266
|
+
if (target.kind === "symbol")
|
|
267
|
+
call.calleeFileRel = target.file;
|
|
268
|
+
else if (target.files.length > 0)
|
|
269
|
+
call.calleeFileRel = target.files[0];
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
call.isExternal = true;
|
|
273
|
+
call.calleeFileRel = importRef.from;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (importRef.from.startsWith(".")) {
|
|
277
|
+
const resolvedAbs = resolveImportPath(importRef.from, filePath);
|
|
278
|
+
if (resolvedAbs) {
|
|
279
|
+
call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
call.isExternal = true;
|
|
284
|
+
call.calleeFileRel = importRef.from;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (aliasOrigin) {
|
|
288
|
+
// Destructured aliases are TS/JS only (always relative or external).
|
|
289
|
+
if (aliasOrigin.startsWith(".")) {
|
|
290
|
+
const resolvedAbs = resolveImportPath(aliasOrigin, filePath);
|
|
190
291
|
if (resolvedAbs) {
|
|
191
292
|
call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
192
293
|
}
|
|
193
294
|
}
|
|
194
295
|
else {
|
|
195
296
|
call.isExternal = true;
|
|
196
|
-
call.calleeFileRel =
|
|
297
|
+
call.calleeFileRel = aliasOrigin;
|
|
197
298
|
}
|
|
198
299
|
}
|
|
199
|
-
else if (
|
|
300
|
+
else if (crossIndex && skel.language === "csharp") {
|
|
301
|
+
// C# `using App.Models;` makes types visible without naming them.
|
|
302
|
+
// Try `<usingNs>.<base>` against the type-by-fqn index.
|
|
303
|
+
for (const ns of skel.imports ?? []) {
|
|
304
|
+
if (!ns.isNamespaceImport)
|
|
305
|
+
continue;
|
|
306
|
+
const f = crossIndex.csharpTypes.get(`${ns.from}.${base}`);
|
|
307
|
+
if (f && f !== skel.file) {
|
|
308
|
+
call.calleeFileRel = f;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!call.calleeFileRel && localNames.has(base))
|
|
313
|
+
call.isLocal = true;
|
|
314
|
+
}
|
|
315
|
+
else if (crossIndex && skel.language === "java") {
|
|
316
|
+
// Java wildcard import: `import com.example.*;` doesn't name the type.
|
|
317
|
+
for (const wc of skel.imports ?? []) {
|
|
318
|
+
if (wc.symbol !== "*")
|
|
319
|
+
continue;
|
|
320
|
+
const f = crossIndex.javaFqcn.get(`${wc.from}.${base}`);
|
|
321
|
+
if (f && f !== skel.file) {
|
|
322
|
+
call.calleeFileRel = f;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (!call.calleeFileRel && localNames.has(base))
|
|
327
|
+
call.isLocal = true;
|
|
328
|
+
}
|
|
329
|
+
else if (localNames.has(base)) {
|
|
200
330
|
call.isLocal = true;
|
|
201
331
|
}
|
|
202
332
|
calls.push(call);
|
|
203
333
|
}
|
|
204
|
-
// calledBy:
|
|
334
|
+
// ── calledBy: who imports this function? ────────────────────────────────
|
|
205
335
|
const calledBy = [];
|
|
206
336
|
if (allSkeletons) {
|
|
207
337
|
for (const otherSkel of allSkeletons) {
|
|
208
338
|
if (otherSkel.file === relPath)
|
|
209
339
|
continue;
|
|
340
|
+
const otherIsCrossLang = CROSS_LANG.has(otherSkel.language);
|
|
341
|
+
const otherAbs = path.resolve(root, otherSkel.file);
|
|
210
342
|
for (const imp of otherSkel.imports ?? []) {
|
|
211
343
|
const importedName = imp.alias ?? imp.symbol;
|
|
212
344
|
if (importedName !== funcName && imp.symbol !== funcName)
|
|
213
345
|
continue;
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
346
|
+
if (otherIsCrossLang) {
|
|
347
|
+
// Symbol-level cross-lang match only — file/namespace edges are too
|
|
348
|
+
// broad to claim "this file calls funcName".
|
|
349
|
+
if (!crossIndex)
|
|
350
|
+
continue;
|
|
351
|
+
const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
|
|
352
|
+
if (target && target.kind === "symbol" && target.file === relPath && target.symbol === funcName) {
|
|
353
|
+
calledBy.push({ file: otherSkel.file });
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (imp.from.startsWith(".")) {
|
|
358
|
+
const resolvedAbs = resolveImportPath(imp.from, otherAbs);
|
|
359
|
+
if (!resolvedAbs)
|
|
360
|
+
continue;
|
|
361
|
+
const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
|
|
362
|
+
if (resolvedRel === relPath) {
|
|
363
|
+
calledBy.push({ file: otherSkel.file });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Extra pass: for C# / Go, the cross-lang resolver gives file-level targets
|
|
371
|
+
// (namespace / package) so the loop above misses callers that only show up
|
|
372
|
+
// via name-resolution at the call site. Scan candidate files' call sites.
|
|
373
|
+
if (allSkeletons &&
|
|
374
|
+
crossIndex &&
|
|
375
|
+
(skel.language === "csharp" || skel.language === "go")) {
|
|
376
|
+
const seenFiles = new Set(calledBy.map((c) => c.file));
|
|
377
|
+
for (const otherSkel of allSkeletons) {
|
|
378
|
+
if (otherSkel.file === relPath)
|
|
379
|
+
continue;
|
|
380
|
+
if (otherSkel.language !== skel.language)
|
|
381
|
+
continue;
|
|
382
|
+
if (seenFiles.has(otherSkel.file))
|
|
383
|
+
continue;
|
|
384
|
+
const otherAbs = path.resolve(root, otherSkel.file);
|
|
385
|
+
// Confirm this other file imports / uses something that resolves to us.
|
|
386
|
+
let importsUs = false;
|
|
387
|
+
for (const imp of otherSkel.imports ?? []) {
|
|
388
|
+
const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
|
|
389
|
+
if (!target)
|
|
219
390
|
continue;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
calledBy.push({ file: otherSkel.file });
|
|
391
|
+
if (target.kind === "file" && target.files.includes(relPath)) {
|
|
392
|
+
importsUs = true;
|
|
223
393
|
break;
|
|
224
394
|
}
|
|
395
|
+
if (target.kind === "symbol" && target.file === relPath) {
|
|
396
|
+
importsUs = true;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!importsUs)
|
|
401
|
+
continue;
|
|
402
|
+
if (await fileCallsSymbol(otherAbs, funcName)) {
|
|
403
|
+
calledBy.push({ file: otherSkel.file });
|
|
404
|
+
seenFiles.add(otherSkel.file);
|
|
225
405
|
}
|
|
226
406
|
}
|
|
227
407
|
}
|