universal-ast-mapper 2.0.0 → 2.0.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +185 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +341 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
@@ -0,0 +1,134 @@
1
+ import path from "node:path";
2
+ // ─── Symbol lookup ────────────────────────────────────────────────────────────
3
+ /** Recursively search for a symbol by name and optional kind. */
4
+ export function findSymbol(symbols, name, kind) {
5
+ for (const sym of symbols) {
6
+ if (sym.name === name && (!kind || sym.kind === kind))
7
+ return sym;
8
+ const found = findSymbol(sym.children, name, kind);
9
+ if (found)
10
+ return found;
11
+ }
12
+ return null;
13
+ }
14
+ /**
15
+ * Given a target symbol with a signature, find related type/interface/enum
16
+ * symbols referenced in that signature and return their source code blocks.
17
+ */
18
+ export function findRelatedSymbols(symbols, target, sourceLines) {
19
+ if (!target.signature)
20
+ return [];
21
+ const seen = new Set([target.name]);
22
+ // PascalCase identifiers in the signature are likely type references
23
+ const typeRefs = [...target.signature.matchAll(/\b([A-Z][a-zA-Z0-9_]*)\b/g)]
24
+ .map((m) => m[1])
25
+ .filter((v) => !seen.has(v) && (seen.add(v), true));
26
+ const related = [];
27
+ for (const typeName of typeRefs) {
28
+ const sym = findSymbol(symbols, typeName);
29
+ if (sym && (sym.kind === "interface" || sym.kind === "type" || sym.kind === "enum")) {
30
+ const code = sourceLines.slice(sym.range.startLine - 1, sym.range.endLine).join("\n");
31
+ related.push({ name: sym.name, kind: sym.kind, range: sym.range, code });
32
+ }
33
+ }
34
+ return related;
35
+ }
36
+ // ─── Architecture validation ──────────────────────────────────────────────────
37
+ /** True if the first 500 chars of source contain the given directive literal. */
38
+ export function hasDirective(source, directive) {
39
+ const head = source.slice(0, 500);
40
+ return head.includes(`"${directive}"`) || head.includes(`'${directive}'`);
41
+ }
42
+ /** Patterns that flag server-only imports in a "use client" file. */
43
+ const SERVER_IMPORT_PATTERNS = [
44
+ { pattern: /from\s+['"]server-only['"]/, label: "server-only" },
45
+ { pattern: /from\s+['"][^'"]*\/prisma['"]/, label: "prisma client" },
46
+ { pattern: /from\s+['"][^'"]*lib\/prisma['"]/, label: "lib/prisma" },
47
+ { pattern: /from\s+['"]next\/headers['"]/, label: "next/headers" },
48
+ { pattern: /from\s+['"]next\/cookies['"]/, label: "next/cookies" },
49
+ { pattern: /from\s+['"][^'"]*lib\/auth['"]/, label: "lib/auth" },
50
+ { pattern: /from\s+['"][^'"]*lib\/auditLog['"]/, label: "lib/auditLog" },
51
+ { pattern: /from\s+['"][^'"]*lib\/apiAuth['"]/, label: "lib/apiAuth" },
52
+ ];
53
+ /** Scan source lines for server-only imports (called on "use client" files). */
54
+ export function findServerImports(source) {
55
+ const lines = source.split("\n");
56
+ const violations = [];
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const line = lines[i];
59
+ for (const { pattern, label } of SERVER_IMPORT_PATTERNS) {
60
+ if (pattern.test(line)) {
61
+ const match = line.match(/from\s+['"]([^'"]+)['"]/);
62
+ violations.push({ module: match ? match[1] : line.trim(), label, line: i + 1 });
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ return violations;
68
+ }
69
+ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
70
+ /** True if the relative path looks like a Next.js App Router API route file. */
71
+ export function isApiRoute(relPath) {
72
+ const norm = relPath.split(path.sep).join("/");
73
+ return /app\/api\/.+\/route\.(ts|js|tsx|jsx)$/.test(norm);
74
+ }
75
+ /**
76
+ * Find exported HTTP handler functions that have no try/catch.
77
+ * A simple but effective heuristic: look for the `try {` keyword in the body.
78
+ */
79
+ export function findMissingTryCatch(symbols, sourceLines) {
80
+ const missing = [];
81
+ for (const sym of symbols) {
82
+ if (!HTTP_METHODS.has(sym.name) || sym.exported === false)
83
+ continue;
84
+ const bodyText = sourceLines.slice(sym.range.startLine - 1, sym.range.endLine).join("\n");
85
+ if (!/\btry\s*\{/.test(bodyText))
86
+ missing.push(sym);
87
+ }
88
+ return missing;
89
+ }
90
+ export const GENERAL_RULE_DEFAULTS = {
91
+ largeFileLines: 500,
92
+ tooManyImports: 15,
93
+ godExportCount: 10,
94
+ };
95
+ /**
96
+ * Run general-purpose structural rules against a source file.
97
+ * Returns violations for any threshold exceeded.
98
+ */
99
+ export function checkGeneralRules(fileRel, source, symbols, importCount, thresholds = GENERAL_RULE_DEFAULTS) {
100
+ const violations = [];
101
+ const lineCount = source.split("\n").length;
102
+ if (lineCount > thresholds.largeFileLines) {
103
+ violations.push({
104
+ file: fileRel,
105
+ rule: "large-file",
106
+ severity: "warning",
107
+ message: `File has ${lineCount} lines (threshold: ${thresholds.largeFileLines}) — consider splitting`,
108
+ value: lineCount,
109
+ threshold: thresholds.largeFileLines,
110
+ });
111
+ }
112
+ if (importCount > thresholds.tooManyImports) {
113
+ violations.push({
114
+ file: fileRel,
115
+ rule: "too-many-imports",
116
+ severity: "warning",
117
+ message: `File has ${importCount} imports (threshold: ${thresholds.tooManyImports}) — high coupling`,
118
+ value: importCount,
119
+ threshold: thresholds.tooManyImports,
120
+ });
121
+ }
122
+ const exportedCount = symbols.filter((s) => s.exported).length;
123
+ if (exportedCount > thresholds.godExportCount) {
124
+ violations.push({
125
+ file: fileRel,
126
+ rule: "god-export",
127
+ severity: "warning",
128
+ message: `File exports ${exportedCount} symbols (threshold: ${thresholds.godExportCount}) — potential God File`,
129
+ value: exportedCount,
130
+ threshold: thresholds.godExportCount,
131
+ });
132
+ }
133
+ return violations;
134
+ }
@@ -0,0 +1,82 @@
1
+ function globToRegex(pattern) {
2
+ let result = "";
3
+ for (let i = 0; i < pattern.length; i++) {
4
+ const c = pattern[i];
5
+ if (c === "*" && pattern[i + 1] === "*") {
6
+ result += ".*";
7
+ i++;
8
+ }
9
+ else if (c === "*") {
10
+ result += "[^/]*";
11
+ }
12
+ else if (c === "?") {
13
+ result += "[^/]";
14
+ }
15
+ else if (/[.+^${}()|[\]\\]/.test(c)) {
16
+ result += "\\" + c;
17
+ }
18
+ else {
19
+ result += c;
20
+ }
21
+ }
22
+ return new RegExp("^" + result + "$");
23
+ }
24
+ function matchGlob(pattern, str) {
25
+ try {
26
+ return globToRegex(pattern).test(str);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function loadArchRules(projectConfig) {
33
+ const arch = projectConfig.arch;
34
+ return arch?.rules ?? [];
35
+ }
36
+ export function checkArchRules(graph, rules) {
37
+ if (rules.length === 0)
38
+ return [];
39
+ const violations = [];
40
+ const fileImports = new Map();
41
+ for (const edge of graph.edges) {
42
+ if (edge.edgeType === "imports") {
43
+ const fromFile = edge.from.split("::")[0];
44
+ const toFile = edge.to.split("::")[0];
45
+ if (!fileImports.has(fromFile))
46
+ fileImports.set(fromFile, new Set());
47
+ fileImports.get(fromFile).add(toFile);
48
+ }
49
+ }
50
+ const allFiles = [...fileImports.keys()];
51
+ for (const rule of rules) {
52
+ const severity = rule.severity ?? "error";
53
+ const fromFiles = allFiles.filter(f => matchGlob(rule.from, f));
54
+ for (const file of fromFiles) {
55
+ const imports = fileImports.get(file) ?? new Set();
56
+ if (rule.forbidImport) {
57
+ for (const imp of imports) {
58
+ if (matchGlob(rule.forbidImport, imp)) {
59
+ violations.push({
60
+ rule: rule.name ?? `forbid: ${rule.from} → ${rule.forbidImport}`,
61
+ severity,
62
+ file,
63
+ message: rule.message ?? `"${file}" must not import "${imp}" (matches ${rule.forbidImport})`,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ if (rule.requireImport) {
69
+ const hasRequired = [...imports].some(imp => matchGlob(rule.requireImport, imp));
70
+ if (!hasRequired) {
71
+ violations.push({
72
+ rule: rule.name ?? `require: ${rule.from} → ${rule.requireImport}`,
73
+ severity,
74
+ file,
75
+ message: rule.message ?? `"${file}" must import something matching "${rule.requireImport}"`,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return violations;
82
+ }
@@ -0,0 +1,467 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parseSource } from "./parser.js";
4
+ import { buildSkeleton } from "./skeleton.js";
5
+ import { resolveOptions, loadProjectConfig } from "./config.js";
6
+ import { detectLanguage } from "./registry.js";
7
+ import { resolveImportPath, resolveAliasedImport, getOrBuildCrossLangIndex } from "./resolver.js";
8
+ import { resolveCrossLangTarget } from "./crosslang.js";
9
+ const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp", "swift"]);
10
+ function pushCall(out, callee, anchor) {
11
+ if (callee && anchor)
12
+ out.push({ callee, line: anchor.startPosition.row + 1 });
13
+ }
14
+ function collectCalls(node, out) {
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") {
19
+ const fn = node.childForFieldName("function");
20
+ if (fn) {
21
+ let callee = null;
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
+ }
54
+ }
55
+ pushCall(out, callee, fn);
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
+ }
71
+ }
72
+ // ── Java method invocation
73
+ else if (t === "method_invocation") {
74
+ const name = node.childForFieldName("name");
75
+ const obj = node.childForFieldName("object");
76
+ if (name)
77
+ pushCall(out, obj ? `${obj.text}.${name.text}` : name.text, name);
78
+ }
79
+ // ── C# invocation expression
80
+ else if (t === "invocation_expression") {
81
+ const fn = node.childForFieldName("function");
82
+ if (fn)
83
+ pushCall(out, fn.text, fn);
84
+ }
85
+ // ── Java + C# constructor call: new Foo(...)
86
+ else if (t === "object_creation_expression") {
87
+ let typeNode = node.childForFieldName("type");
88
+ if (!typeNode) {
89
+ for (let i = 0; i < node.namedChildCount; i++) {
90
+ const c = node.namedChild(i);
91
+ if (c &&
92
+ (c.type === "identifier" ||
93
+ c.type === "type_identifier" ||
94
+ c.type === "scoped_identifier" ||
95
+ c.type === "qualified_name" ||
96
+ c.type === "generic_type")) {
97
+ typeNode = c;
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ if (typeNode)
103
+ pushCall(out, `new ${typeNode.text}`, typeNode);
104
+ }
105
+ for (let i = 0; i < node.namedChildCount; i++) {
106
+ const c = node.namedChild(i);
107
+ if (c)
108
+ collectCalls(c, out);
109
+ }
110
+ }
111
+ // ─── Function-node finder ─────────────────────────────────────────────────────
112
+ const FUNCTION_NODE_TYPES = new Set([
113
+ "function_declaration", // TS / JS / Go
114
+ "generator_function_declaration",
115
+ "method_definition", // TS / JS class member
116
+ "method_signature",
117
+ "abstract_method_signature",
118
+ "function_definition", // Python
119
+ "async_function_definition", // Python async
120
+ "method_declaration", // Go / Java / C#
121
+ "constructor_declaration", // Java / C#
122
+ "function_item", // Rust
123
+ ]);
124
+ function findFunctionNode(root, name) {
125
+ function walk(node) {
126
+ if (FUNCTION_NODE_TYPES.has(node.type)) {
127
+ const named = node.childForFieldName("name");
128
+ if (named?.text === name)
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
+ }
137
+ }
138
+ // const foo = () => ... | const foo = function() ...
139
+ if (node.type === "variable_declarator") {
140
+ const declName = node.childForFieldName("name")?.text;
141
+ const value = node.childForFieldName("value");
142
+ if (declName === name &&
143
+ value &&
144
+ (value.type === "arrow_function" ||
145
+ value.type === "function" ||
146
+ value.type === "function_expression")) {
147
+ return value;
148
+ }
149
+ }
150
+ for (let i = 0; i < node.namedChildCount; i++) {
151
+ const c = node.namedChild(i);
152
+ if (c) {
153
+ const found = walk(c);
154
+ if (found)
155
+ return found;
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ return walk(root);
161
+ }
162
+ // ─── Destructuring alias tracker (TS/JS only) ─────────────────────────────────
163
+ function collectDestructuredAliases(node, importMap) {
164
+ const aliases = new Map();
165
+ function walk(n) {
166
+ if (n.type === "variable_declarator") {
167
+ const nameNode = n.childForFieldName("name");
168
+ const valueNode = n.childForFieldName("value");
169
+ if (nameNode && valueNode && nameNode.type === "object_pattern") {
170
+ const baseName = valueNode.text.split(".")[0];
171
+ const originRef = importMap.get(baseName);
172
+ const origin = originRef?.from ?? aliases.get(baseName);
173
+ if (origin) {
174
+ for (let i = 0; i < nameNode.namedChildCount; i++) {
175
+ const prop = nameNode.namedChild(i);
176
+ if (!prop)
177
+ continue;
178
+ if (prop.type === "shorthand_property_identifier_pattern" ||
179
+ prop.type === "shorthand_property_identifier") {
180
+ aliases.set(prop.text, origin);
181
+ }
182
+ if (prop.type === "pair_pattern") {
183
+ const val = prop.childForFieldName("value");
184
+ if (val)
185
+ aliases.set(val.text, origin);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ for (let i = 0; i < n.namedChildCount; i++) {
192
+ const c = n.namedChild(i);
193
+ if (c)
194
+ walk(c);
195
+ }
196
+ }
197
+ walk(node);
198
+ return aliases;
199
+ }
200
+ // ─── Base identifier of a callee expression ───────────────────────────────────
201
+ /** Take the leftmost identifier from "obj.method" / "Pkg::func" / "new Foo". */
202
+ function baseNameOf(callee) {
203
+ let s = callee;
204
+ if (s.startsWith("new "))
205
+ s = s.slice(4);
206
+ return s.split(/::|\./)[0];
207
+ }
208
+ // ─── Cross-language calledBy scan helper ──────────────────────────────────────
209
+ /** Last segment of a member-style callee — "Helper.fmt" -> "fmt", "compute" -> null. */
210
+ function memberOf(callee) {
211
+ const noNew = callee.startsWith("new ") ? callee.slice(4) : callee;
212
+ const parts = noNew.split(/::|\./);
213
+ return parts.length > 1 ? parts[parts.length - 1] : null;
214
+ }
215
+ /**
216
+ * Open a file, parse it, and check whether any call expression references
217
+ * `funcName` — either as a bare call `funcName(...)` or as the trailing
218
+ * member of a qualified call `X.funcName(...)` / `X::funcName(...)`.
219
+ * Used for C# / Go reverse calledBy where namespace/package imports do not
220
+ * name the called symbol.
221
+ */
222
+ async function fileCallsSymbol(fileAbs, funcName) {
223
+ const lang = detectLanguage(fileAbs);
224
+ if (!lang)
225
+ return false;
226
+ let src;
227
+ try {
228
+ src = fs.readFileSync(fileAbs, "utf8");
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ const root = await parseSource(lang.grammar, src);
234
+ const calls = [];
235
+ collectCalls(root, calls);
236
+ for (const c of calls) {
237
+ if (c.callee === funcName)
238
+ return true;
239
+ const m = memberOf(c.callee);
240
+ if (m === funcName)
241
+ return true;
242
+ }
243
+ return false;
244
+ }
245
+ // ─── Public API ───────────────────────────────────────────────────────────────
246
+ /** Recursively find the first symbol with the given name and return its decorators. */
247
+ function findDecorators(symbols, name) {
248
+ for (const s of symbols) {
249
+ if (s.name === name && s.decorators && s.decorators.length > 0)
250
+ return s.decorators;
251
+ const nested = findDecorators(s.children, name);
252
+ if (nested)
253
+ return nested;
254
+ }
255
+ return undefined;
256
+ }
257
+ export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
258
+ const langEntry = detectLanguage(filePath);
259
+ if (!langEntry)
260
+ return null;
261
+ const source = fs.readFileSync(filePath, "utf8");
262
+ const relPath = path.relative(root, filePath).split(path.sep).join("/");
263
+ const rootNode = await parseSource(langEntry.grammar, source);
264
+ const funcNode = findFunctionNode(rootNode, funcName);
265
+ if (!funcNode)
266
+ return null;
267
+ const body = funcNode.childForFieldName("body") ?? funcNode;
268
+ const rawCalls = [];
269
+ collectCalls(body, rawCalls);
270
+ const opts = resolveOptions({ detail: "outline", emitHtml: false }, loadProjectConfig(root));
271
+ const skel = await buildSkeleton(filePath, relPath, opts);
272
+ // localName -> full ImportRef (so cross-lang resolution has the flags it needs)
273
+ const importMap = new Map();
274
+ for (const imp of skel.imports ?? []) {
275
+ if (imp.symbol !== "*" && !imp.isSideEffect) {
276
+ importMap.set(imp.alias ?? imp.symbol, imp);
277
+ }
278
+ }
279
+ const localNames = new Set(skel.symbols.map((s) => s.name));
280
+ const destructuredAliases = collectDestructuredAliases(body, importMap);
281
+ // Build cross-lang index lazily — needed for Java/C#/Rust dispatch.
282
+ const isCrossLang = CROSS_LANG.has(skel.language);
283
+ const crossIndex = isCrossLang ? await getOrBuildCrossLangIndex(root) : null;
284
+ const calls = [];
285
+ const seen = new Set();
286
+ for (const { callee, line } of rawCalls) {
287
+ const key = `${callee}:${line}`;
288
+ if (seen.has(key))
289
+ continue;
290
+ seen.add(key);
291
+ const base = baseNameOf(callee);
292
+ const importRef = importMap.get(base);
293
+ const aliasOrigin = destructuredAliases.get(base);
294
+ const call = { callee, line };
295
+ if (importRef) {
296
+ if (isCrossLang && crossIndex) {
297
+ const target = resolveCrossLangTarget(importRef, skel, filePath, root, crossIndex);
298
+ if (target) {
299
+ if (target.kind === "symbol")
300
+ call.calleeFileRel = target.file;
301
+ else if (target.files.length > 0)
302
+ call.calleeFileRel = target.files[0];
303
+ }
304
+ else {
305
+ call.isExternal = true;
306
+ call.calleeFileRel = importRef.from;
307
+ }
308
+ }
309
+ else if (importRef.from.startsWith(".")) {
310
+ const resolvedAbs = resolveImportPath(importRef.from, filePath);
311
+ if (resolvedAbs) {
312
+ call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
313
+ }
314
+ }
315
+ else {
316
+ const aliasAbs = resolveAliasedImport(importRef.from, filePath);
317
+ if (aliasAbs) {
318
+ call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
319
+ }
320
+ else {
321
+ call.isExternal = true;
322
+ call.calleeFileRel = importRef.from;
323
+ }
324
+ }
325
+ }
326
+ else if (aliasOrigin) {
327
+ // Destructured aliases are TS/JS only (always relative or external).
328
+ if (aliasOrigin.startsWith(".")) {
329
+ const resolvedAbs = resolveImportPath(aliasOrigin, filePath);
330
+ if (resolvedAbs) {
331
+ call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
332
+ }
333
+ }
334
+ else {
335
+ const aliasAbs = resolveAliasedImport(aliasOrigin, filePath);
336
+ if (aliasAbs) {
337
+ call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
338
+ }
339
+ else {
340
+ call.isExternal = true;
341
+ call.calleeFileRel = aliasOrigin;
342
+ }
343
+ }
344
+ }
345
+ else if (crossIndex && skel.language === "csharp") {
346
+ // C# `using App.Models;` makes types visible without naming them.
347
+ // Try `<usingNs>.<base>` against the type-by-fqn index.
348
+ for (const ns of skel.imports ?? []) {
349
+ if (!ns.isNamespaceImport)
350
+ continue;
351
+ const f = crossIndex.csharpTypes.get(`${ns.from}.${base}`);
352
+ if (f && f !== skel.file) {
353
+ call.calleeFileRel = f;
354
+ break;
355
+ }
356
+ }
357
+ if (!call.calleeFileRel && localNames.has(base))
358
+ call.isLocal = true;
359
+ }
360
+ else if (crossIndex && skel.language === "java") {
361
+ // Java wildcard import: `import com.example.*;` doesn't name the type.
362
+ for (const wc of skel.imports ?? []) {
363
+ if (wc.symbol !== "*")
364
+ continue;
365
+ const f = crossIndex.javaFqcn.get(`${wc.from}.${base}`);
366
+ if (f && f !== skel.file) {
367
+ call.calleeFileRel = f;
368
+ break;
369
+ }
370
+ }
371
+ if (!call.calleeFileRel && localNames.has(base))
372
+ call.isLocal = true;
373
+ }
374
+ else if (localNames.has(base)) {
375
+ call.isLocal = true;
376
+ }
377
+ calls.push(call);
378
+ }
379
+ // ── calledBy: who imports this function? ────────────────────────────────
380
+ const calledBy = [];
381
+ if (allSkeletons) {
382
+ for (const otherSkel of allSkeletons) {
383
+ if (otherSkel.file === relPath)
384
+ continue;
385
+ const otherIsCrossLang = CROSS_LANG.has(otherSkel.language);
386
+ const otherAbs = path.resolve(root, otherSkel.file);
387
+ for (const imp of otherSkel.imports ?? []) {
388
+ const importedName = imp.alias ?? imp.symbol;
389
+ if (importedName !== funcName && imp.symbol !== funcName)
390
+ continue;
391
+ if (otherIsCrossLang) {
392
+ // Symbol-level cross-lang match only — file/namespace edges are too
393
+ // broad to claim "this file calls funcName".
394
+ if (!crossIndex)
395
+ continue;
396
+ const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
397
+ if (target && target.kind === "symbol" && target.file === relPath && target.symbol === funcName) {
398
+ calledBy.push({ file: otherSkel.file });
399
+ break;
400
+ }
401
+ }
402
+ else {
403
+ const resolvedAbs = imp.from.startsWith(".")
404
+ ? resolveImportPath(imp.from, otherAbs)
405
+ : resolveAliasedImport(imp.from, otherAbs);
406
+ if (!resolvedAbs)
407
+ continue;
408
+ const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
409
+ if (resolvedRel === relPath) {
410
+ calledBy.push({ file: otherSkel.file });
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+ // Extra pass: for C# / Go, the cross-lang resolver gives file-level targets
418
+ // (namespace / package) so the loop above misses callers that only show up
419
+ // via name-resolution at the call site. Scan candidate files' call sites.
420
+ if (allSkeletons &&
421
+ crossIndex &&
422
+ (skel.language === "csharp" || skel.language === "go")) {
423
+ const seenFiles = new Set(calledBy.map((c) => c.file));
424
+ for (const otherSkel of allSkeletons) {
425
+ if (otherSkel.file === relPath)
426
+ continue;
427
+ if (otherSkel.language !== skel.language)
428
+ continue;
429
+ if (seenFiles.has(otherSkel.file))
430
+ continue;
431
+ const otherAbs = path.resolve(root, otherSkel.file);
432
+ // Confirm this other file imports / uses something that resolves to us.
433
+ let importsUs = false;
434
+ for (const imp of otherSkel.imports ?? []) {
435
+ const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
436
+ if (!target)
437
+ continue;
438
+ if (target.kind === "file" && target.files.includes(relPath)) {
439
+ importsUs = true;
440
+ break;
441
+ }
442
+ if (target.kind === "symbol" && target.file === relPath) {
443
+ importsUs = true;
444
+ break;
445
+ }
446
+ }
447
+ if (!importsUs)
448
+ continue;
449
+ if (await fileCallsSymbol(otherAbs, funcName)) {
450
+ calledBy.push({ file: otherSkel.file });
451
+ seenFiles.add(otherSkel.file);
452
+ }
453
+ }
454
+ }
455
+ const decorators = findDecorators(skel.symbols, funcName);
456
+ return {
457
+ file: relPath,
458
+ function: funcName,
459
+ functionRange: {
460
+ startLine: funcNode.startPosition.row + 1,
461
+ endLine: funcNode.endPosition.row + 1,
462
+ },
463
+ ...(decorators ? { decorators } : {}),
464
+ calls,
465
+ calledBy,
466
+ };
467
+ }