devlensio 1.0.4 → 1.1.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/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.js +11 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/types.d.ts +6 -0
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +1 -1
|
@@ -1,8 +1,89 @@
|
|
|
1
1
|
import { closestByPath } from "./utils.js";
|
|
2
|
+
// Arity compatibility: a candidate matches when the call provides at least all
|
|
3
|
+
// required (non-optional, non-rest) params, and does not exceed the declared
|
|
4
|
+
// count unless the candidate declares a rest parameter. Candidates without
|
|
5
|
+
// parameters metadata are permissive (unknown shape).
|
|
6
|
+
function arityMatches(candidate, argCount) {
|
|
7
|
+
const parameters = candidate.metadata?.parameters;
|
|
8
|
+
const params = candidate.metadata?.params;
|
|
9
|
+
if (!params && !parameters)
|
|
10
|
+
return true;
|
|
11
|
+
const total = parameters?.length ?? params?.length ?? 0;
|
|
12
|
+
const hasRest = parameters?.some((p) => p.isRest) ?? false;
|
|
13
|
+
const required = parameters
|
|
14
|
+
? parameters.filter((p) => !p.isOptional && !p.isRest).length
|
|
15
|
+
: total;
|
|
16
|
+
return argCount >= required && (hasRest || argCount <= total);
|
|
17
|
+
}
|
|
18
|
+
// Loose type agreement between a call-site arg type tag and a declared param
|
|
19
|
+
// type text. Both sides are cheap/heuristic (no type checker), so this scores
|
|
20
|
+
// rather than gates: unknown tags never match, text containment counts.
|
|
21
|
+
function typeAgrees(argType, paramType) {
|
|
22
|
+
if (!argType || argType === "unknown" || !paramType)
|
|
23
|
+
return false;
|
|
24
|
+
const p = paramType.trim();
|
|
25
|
+
return p === argType || p.includes(argType);
|
|
26
|
+
}
|
|
27
|
+
function typeScore(candidate, argTypes) {
|
|
28
|
+
const parameters = candidate.metadata?.parameters;
|
|
29
|
+
if (!parameters)
|
|
30
|
+
return 0;
|
|
31
|
+
let score = 0;
|
|
32
|
+
for (let i = 0; i < argTypes.length; i++) {
|
|
33
|
+
if (parameters[i] && typeAgrees(argTypes[i], parameters[i].type))
|
|
34
|
+
score++;
|
|
35
|
+
}
|
|
36
|
+
return score;
|
|
37
|
+
}
|
|
38
|
+
// Picks the right overload among same-name candidates using the call site's
|
|
39
|
+
// argument shape. Ladder:
|
|
40
|
+
// 1. no usable call shape (USES edges, spread calls, missing callSites)
|
|
41
|
+
// → legacy closestByPath behavior
|
|
42
|
+
// 2. arity filter — survivors whose declared params accept argCount
|
|
43
|
+
// 3. type tiebreak — among arity survivors, prefer the candidate with the
|
|
44
|
+
// highest arg-type agreement (handles same arity, different types)
|
|
45
|
+
// 4. still ambiguous → closestByPath among survivors
|
|
46
|
+
function resolveOverloadTarget(candidates, caller, callSite) {
|
|
47
|
+
if (candidates.length === 1)
|
|
48
|
+
return { target: candidates[0], matchedBy: "name" };
|
|
49
|
+
const usableShape = callSite
|
|
50
|
+
&& typeof callSite.argCount === "number"
|
|
51
|
+
&& !callSite.hasSpread;
|
|
52
|
+
if (!usableShape)
|
|
53
|
+
return { target: closestByPath(candidates, caller.filePath), matchedBy: "name" };
|
|
54
|
+
let pool = candidates.filter((c) => arityMatches(c, callSite.argCount));
|
|
55
|
+
if (pool.length === 0)
|
|
56
|
+
pool = candidates; // nothing accepts this arity — keep all
|
|
57
|
+
if (pool.length === 1)
|
|
58
|
+
return { target: pool[0], matchedBy: "arity" };
|
|
59
|
+
const argTypes = callSite.argTypes ?? [];
|
|
60
|
+
if (argTypes.some((t) => t && t !== "unknown")) {
|
|
61
|
+
let best = pool[0];
|
|
62
|
+
let bestScore = typeScore(pool[0], argTypes);
|
|
63
|
+
let tieAtBest = 1;
|
|
64
|
+
for (let i = 1; i < pool.length; i++) {
|
|
65
|
+
const s = typeScore(pool[i], argTypes);
|
|
66
|
+
if (s > bestScore) {
|
|
67
|
+
best = pool[i];
|
|
68
|
+
bestScore = s;
|
|
69
|
+
tieAtBest = 1;
|
|
70
|
+
}
|
|
71
|
+
else if (s === bestScore)
|
|
72
|
+
tieAtBest++;
|
|
73
|
+
}
|
|
74
|
+
if (bestScore > 0 && tieAtBest === 1) {
|
|
75
|
+
return { target: best, matchedBy: "signature" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { target: closestByPath(pool, caller.filePath), matchedBy: "name" };
|
|
79
|
+
}
|
|
2
80
|
export function detectCallEdges(nodes, lookupMp) {
|
|
3
81
|
const edges = [];
|
|
4
82
|
// Dedup for THIRD_PARTY CALLS edges — one per (caller, target) pair
|
|
5
83
|
const createdThirdPartyEdges = new Set();
|
|
84
|
+
// Dedup for local CALLS/USES edges — overload call sites can resolve to
|
|
85
|
+
// the same target through different (name, arity) pairs
|
|
86
|
+
const createdLocalEdges = new Set();
|
|
6
87
|
// Accumulate resolved calls per node — written back to metadata after edge loop
|
|
7
88
|
const resolvedCallsMap = new Map();
|
|
8
89
|
// Lazily-created method nodes for default/namespace import member-access calls
|
|
@@ -34,7 +115,16 @@ export function detectCallEdges(nodes, lookupMp) {
|
|
|
34
115
|
if (!hasPrimary && !hasHooks)
|
|
35
116
|
continue;
|
|
36
117
|
// ── Primary names: both third-party and local edges ──────────────
|
|
37
|
-
|
|
118
|
+
// Structured callSites carry the argument shape for overload-aware
|
|
119
|
+
// resolution; fall back to bare calls/uses strings when absent.
|
|
120
|
+
const callSites = node.metadata.callSites;
|
|
121
|
+
// Each (name, arity) call site iterates separately — a caller that
|
|
122
|
+
// invokes two overloads of one name gets two edges.
|
|
123
|
+
const primaryCallSites = (callSites && callSites.length > 0)
|
|
124
|
+
? callSites
|
|
125
|
+
: (primaryNames ?? []).map((name) => ({ name, argCount: -1 }));
|
|
126
|
+
for (const callSite of primaryCallSites) {
|
|
127
|
+
const calledName = callSite.name;
|
|
38
128
|
// ── Third-party guard ─────────────────────────────────────────
|
|
39
129
|
// The alias map is keyed by node.filePath (relative) and populated
|
|
40
130
|
// by importEdges.ts (which runs first).
|
|
@@ -120,16 +210,18 @@ export function detectCallEdges(nodes, lookupMp) {
|
|
|
120
210
|
}
|
|
121
211
|
if (!targets || targets.length === 0)
|
|
122
212
|
continue;
|
|
123
|
-
const target = targets.
|
|
124
|
-
? targets[0]
|
|
125
|
-
: closestByPath(targets, node.filePath);
|
|
213
|
+
const { target, matchedBy } = resolveOverloadTarget(targets, node, callSite.argCount >= 0 ? callSite : undefined);
|
|
126
214
|
if (target.id === node.id)
|
|
127
215
|
continue; // skip self-reference
|
|
216
|
+
const localEdgeKey = `${node.id}→${target.id}:${edgeType}`;
|
|
217
|
+
if (createdLocalEdges.has(localEdgeKey))
|
|
218
|
+
continue;
|
|
219
|
+
createdLocalEdges.add(localEdgeKey);
|
|
128
220
|
edges.push({
|
|
129
221
|
from: node.id,
|
|
130
222
|
to: target.id,
|
|
131
223
|
type: edgeType,
|
|
132
|
-
metadata: { calledName: resolvedName },
|
|
224
|
+
metadata: { calledName: resolvedName, matchedBy },
|
|
133
225
|
});
|
|
134
226
|
if (!resolvedCallsMap.has(node.id))
|
|
135
227
|
resolvedCallsMap.set(node.id, []);
|
|
@@ -198,3 +198,157 @@ describe("detectCallEdges with class methods", () => {
|
|
|
198
198
|
deleteFakeRepo(repo);
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
|
+
// ─── Overload-aware resolution (same-name candidates in one file) ─────────────
|
|
202
|
+
describe("detectCallEdges with overloads", () => {
|
|
203
|
+
// Arity disambiguates same-name functions in one file
|
|
204
|
+
it("routes each call to the overload matching its argument count", () => {
|
|
205
|
+
const repo = createFakeRepo({
|
|
206
|
+
"src/fmt.ts": `
|
|
207
|
+
export function formatValue(v: string) { return v.trim(); }
|
|
208
|
+
export function formatValue(v: string, n: number) { return v.repeat(n); }
|
|
209
|
+
export function caller() {
|
|
210
|
+
formatValue("a");
|
|
211
|
+
formatValue("a", 3);
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
});
|
|
215
|
+
const { edges } = detectCalls(repo);
|
|
216
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "formatValue");
|
|
217
|
+
const targets = callEdges.map((e) => e.to).sort();
|
|
218
|
+
expect(targets).toEqual([
|
|
219
|
+
"src/fmt.ts::formatValue#1",
|
|
220
|
+
"src/fmt.ts::formatValue#2",
|
|
221
|
+
]);
|
|
222
|
+
// resolution confidence recorded on the edge
|
|
223
|
+
expect(callEdges.every((e) => e.metadata?.matchedBy === "arity")).toBe(true);
|
|
224
|
+
deleteFakeRepo(repo);
|
|
225
|
+
});
|
|
226
|
+
// Same arity, different param types — literal arg types break the tie
|
|
227
|
+
it("resolves same-arity overloads via argument type tags", () => {
|
|
228
|
+
const repo = createFakeRepo({
|
|
229
|
+
"src/dec.ts": `
|
|
230
|
+
export function decode(input: string, mode: string) { return input; }
|
|
231
|
+
export function decode(input: number, mode: string) { return String(input); }
|
|
232
|
+
export function caller() {
|
|
233
|
+
decode("abc", "utf8");
|
|
234
|
+
decode(42, "utf8");
|
|
235
|
+
}
|
|
236
|
+
`,
|
|
237
|
+
});
|
|
238
|
+
const { edges } = detectCalls(repo);
|
|
239
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "decode");
|
|
240
|
+
// first call (string literal) → string overload; second (number literal) → number overload
|
|
241
|
+
const stringTargets = callEdges.filter((e) => e.metadata?.matchedBy === "signature").map((e) => e.to).sort();
|
|
242
|
+
expect(stringTargets.length).toBe(2);
|
|
243
|
+
expect(stringTargets[0]).not.toBe(stringTargets[1]);
|
|
244
|
+
deleteFakeRepo(repo);
|
|
245
|
+
});
|
|
246
|
+
// A caller can invoke two overloads of one name — two distinct edges
|
|
247
|
+
it("emits one edge per overload call site, not one per name", () => {
|
|
248
|
+
const repo = createFakeRepo({
|
|
249
|
+
"src/multi.ts": `
|
|
250
|
+
export function send(x: string) { return x; }
|
|
251
|
+
export function send(x: string, y: string) { return x + y; }
|
|
252
|
+
export function client() {
|
|
253
|
+
send("a");
|
|
254
|
+
send("a", "b");
|
|
255
|
+
}
|
|
256
|
+
`,
|
|
257
|
+
});
|
|
258
|
+
const { edges } = detectCalls(repo);
|
|
259
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "send");
|
|
260
|
+
expect(callEdges.length).toBe(2);
|
|
261
|
+
expect(new Set(callEdges.map((e) => e.to)).size).toBe(2);
|
|
262
|
+
deleteFakeRepo(repo);
|
|
263
|
+
});
|
|
264
|
+
// Spread calls have unknowable arity — must NOT confidently pick an overload
|
|
265
|
+
it("falls back to path proximity for spread calls", () => {
|
|
266
|
+
const repo = createFakeRepo({
|
|
267
|
+
"src/spread.ts": `
|
|
268
|
+
export function combine(a: string) { return a; }
|
|
269
|
+
export function combine(a: string, b: string) { return a + b; }
|
|
270
|
+
export function caller(...args: string[]) {
|
|
271
|
+
return combine(...args);
|
|
272
|
+
}
|
|
273
|
+
`,
|
|
274
|
+
});
|
|
275
|
+
const { edges } = detectCalls(repo);
|
|
276
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "combine");
|
|
277
|
+
expect(callEdges.length).toBe(1);
|
|
278
|
+
expect(callEdges[0].metadata?.matchedBy).toBe("name");
|
|
279
|
+
deleteFakeRepo(repo);
|
|
280
|
+
});
|
|
281
|
+
// Optional params: a 1-arg call may hit a 2-param overload with optional 2nd
|
|
282
|
+
it("lets optional parameters accept shorter calls via arity fallback", () => {
|
|
283
|
+
const repo = createFakeRepo({
|
|
284
|
+
"src/opt.ts": `
|
|
285
|
+
export function render(template: string, opts?: object) { return template; }
|
|
286
|
+
export function render(template: string, opts: object, mode: string) { return template; }
|
|
287
|
+
export function caller() {
|
|
288
|
+
render("t");
|
|
289
|
+
}
|
|
290
|
+
`,
|
|
291
|
+
});
|
|
292
|
+
const { edges } = detectCalls(repo);
|
|
293
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "render");
|
|
294
|
+
// 1-arg call: renderStrict requires 3 → excluded; render (optional) → match
|
|
295
|
+
expect(callEdges.length).toBe(1);
|
|
296
|
+
expect(callEdges[0].to).toBe("src/opt.ts::render#2");
|
|
297
|
+
deleteFakeRepo(repo);
|
|
298
|
+
});
|
|
299
|
+
// Rest params accept any extra arguments
|
|
300
|
+
it("lets rest parameters accept longer calls", () => {
|
|
301
|
+
const repo = createFakeRepo({
|
|
302
|
+
"src/rest.ts": `
|
|
303
|
+
export function sum(first: number, ...rest: number[]) { return first; }
|
|
304
|
+
export function pair(a: number, b: number) { return a; }
|
|
305
|
+
export function caller() {
|
|
306
|
+
sum(1, 2, 3, 4);
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
});
|
|
310
|
+
const { edges } = detectCalls(repo);
|
|
311
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "sum");
|
|
312
|
+
expect(callEdges.length).toBe(1);
|
|
313
|
+
expect(callEdges[0].to).toContain("sum");
|
|
314
|
+
deleteFakeRepo(repo);
|
|
315
|
+
});
|
|
316
|
+
// Legacy behavior preserved when no overload shape exists
|
|
317
|
+
it("still resolves plain single-candidate calls without callSites metadata", () => {
|
|
318
|
+
const repo = createFakeRepo({
|
|
319
|
+
"src/plain.ts": `
|
|
320
|
+
export function helper(x: number) { return x; }
|
|
321
|
+
export function caller() { return helper(1); }
|
|
322
|
+
`,
|
|
323
|
+
});
|
|
324
|
+
const { edges } = detectCalls(repo);
|
|
325
|
+
expect(edges).toContainEqual(expect.objectContaining({
|
|
326
|
+
from: "src/plain.ts::caller",
|
|
327
|
+
to: "src/plain.ts::helper",
|
|
328
|
+
type: "CALLS",
|
|
329
|
+
}));
|
|
330
|
+
deleteFakeRepo(repo);
|
|
331
|
+
});
|
|
332
|
+
// Class method overloads resolve through dotted names
|
|
333
|
+
it("resolves class method overloads by arity through dotted names", () => {
|
|
334
|
+
const repo = createFakeRepo({
|
|
335
|
+
"src/svc.ts": `
|
|
336
|
+
export class Repo {
|
|
337
|
+
find(id: string) { return id; }
|
|
338
|
+
find(id: string, depth: number) { return id; }
|
|
339
|
+
}
|
|
340
|
+
export function caller() {
|
|
341
|
+
Repo.find("a");
|
|
342
|
+
Repo.find("a", 2);
|
|
343
|
+
}
|
|
344
|
+
`,
|
|
345
|
+
});
|
|
346
|
+
const { edges } = detectCalls(repo);
|
|
347
|
+
const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "Repo.find");
|
|
348
|
+
expect(callEdges.map((e) => e.to).sort()).toEqual([
|
|
349
|
+
"src/svc.ts::Repo.find#1",
|
|
350
|
+
"src/svc.ts::Repo.find#2",
|
|
351
|
+
]);
|
|
352
|
+
deleteFakeRepo(repo);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Out of scope (deliberate): INTERFACE / ENUM / STRUCT nodes for JS/TS.
|
|
18
18
|
import { SyntaxKind } from "ts-morph";
|
|
19
19
|
import { detectFunctionDirective } from "../directives.js";
|
|
20
|
-
import {
|
|
20
|
+
import { extractCallsWithSites, hasErrorHandling, extractThrowStatements, } from "./functions.js";
|
|
21
21
|
import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
22
22
|
// React lifecycle methods (incl. render) — flagged so consumers can tell
|
|
23
23
|
// framework-registered callbacks apart from ordinary methods.
|
|
@@ -79,6 +79,12 @@ function rewriteThisCalls(calls, className, methodNames) {
|
|
|
79
79
|
return `${className}.${rest.replace(/^#/, "")}`;
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
// Same rewrite as rewriteThisCalls but for structured CallSite records —
|
|
83
|
+
// the name must match the dotted METHOD node names for overload-aware
|
|
84
|
+
// resolution in callEdges.
|
|
85
|
+
function rewriteThisCallSites(callSites, className, methodNames) {
|
|
86
|
+
return callSites.map((cs) => ({ ...cs, name: rewriteThisCalls([cs.name], className, methodNames)[0] }));
|
|
87
|
+
}
|
|
82
88
|
function extractDecoratorNames(cls) {
|
|
83
89
|
if (typeof cls.getDecorators !== "function")
|
|
84
90
|
return [];
|
|
@@ -96,9 +102,8 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
96
102
|
const filePath = file.getFilePath();
|
|
97
103
|
const dotted = `${className}.${methodName}`;
|
|
98
104
|
const typedParams = extractParams(method);
|
|
99
|
-
const calls =
|
|
100
|
-
const
|
|
101
|
-
const apiCalls = extractApiCalls(method);
|
|
105
|
+
const { calls, callSites: rawSites, hookCalls, apiCalls } = extractCallsWithSites(method);
|
|
106
|
+
const callSites = rewriteThisCallSites(rawSites, className, methodNameSet);
|
|
102
107
|
const isAsync = typeof method.isAsync === "function" && method.isAsync();
|
|
103
108
|
const hasErrors = hasErrorHandling(method);
|
|
104
109
|
const throws = extractThrowStatements(method);
|
|
@@ -106,6 +111,7 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
106
111
|
const returnType = extractReturnTypeAnnotation(method);
|
|
107
112
|
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
108
113
|
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
114
|
+
const isOverloadSignature = typeof method.getBody === "function" ? !method.getBody() : false;
|
|
109
115
|
return {
|
|
110
116
|
id: makeId(filePath, dotted),
|
|
111
117
|
name: dotted,
|
|
@@ -121,6 +127,8 @@ function buildMethodNode(file, className, methodName, method, methodNameSet, fil
|
|
|
121
127
|
returnType,
|
|
122
128
|
referencedTypes,
|
|
123
129
|
calls: rewriteThisCalls(calls, className, methodNameSet),
|
|
130
|
+
callSites,
|
|
131
|
+
...(isOverloadSignature && { isOverloadSignature: true }),
|
|
124
132
|
hookCalls,
|
|
125
133
|
apiCalls,
|
|
126
134
|
isAsync,
|
|
@@ -216,9 +224,20 @@ export function extractClasses(file, fileDirective = null) {
|
|
|
216
224
|
if (isHashPrivate)
|
|
217
225
|
node.metadata.isPrivate = true;
|
|
218
226
|
nodes.push(node);
|
|
227
|
+
// Overload signatures — getMethods() returns only implementations;
|
|
228
|
+
// signatures hang off getOverloads() and become body-less METHOD nodes.
|
|
229
|
+
for (const overload of m.getOverloads?.() ?? []) {
|
|
230
|
+
const ovNode = buildMethodNode(file, name, methodName, overload, methodNameSet, fileDirective);
|
|
231
|
+
if (isHashPrivate)
|
|
232
|
+
ovNode.metadata.isPrivate = true;
|
|
233
|
+
nodes.push(ovNode);
|
|
234
|
+
}
|
|
219
235
|
}
|
|
220
236
|
for (const c of cls.getConstructors()) {
|
|
221
237
|
nodes.push(buildMethodNode(file, name, "constructor", c, methodNameSet, fileDirective));
|
|
238
|
+
for (const overload of c.getOverloads?.() ?? []) {
|
|
239
|
+
nodes.push(buildMethodNode(file, name, "constructor", overload, methodNameSet, fileDirective));
|
|
240
|
+
}
|
|
222
241
|
}
|
|
223
242
|
for (const acc of cls.getGetAccessors()) {
|
|
224
243
|
const accName = acc.getName();
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { SourceFile } from "ts-morph";
|
|
2
|
-
import type { CodeNode } from "../../types.js";
|
|
2
|
+
import type { CallSite, CodeNode } from "../../types.js";
|
|
3
3
|
import { type RenderingBoundary } from "../directives.js";
|
|
4
4
|
export declare const JS_BUILTINS: Set<string>;
|
|
5
|
+
export interface ExtractedCallData {
|
|
6
|
+
calls: string[];
|
|
7
|
+
callSites: CallSite[];
|
|
8
|
+
hookCalls: string[];
|
|
9
|
+
apiCalls: string[];
|
|
10
|
+
dependencyNames: string[];
|
|
11
|
+
contextRefs: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function extractCallsWithSites(node: any): ExtractedCallData;
|
|
5
14
|
export declare function extractFunctionCalls(node: any): string[];
|
|
6
|
-
export declare function extractHookCalls(node: any): string[];
|
|
7
|
-
export declare function extractApiCalls(node: any): string[];
|
|
8
15
|
export declare function hasErrorHandling(node: any): boolean;
|
|
9
16
|
export declare function extractThrowStatements(node: any): boolean;
|
|
10
17
|
export declare function extractFunctions(file: SourceFile, fileDirective?: RenderingBoundary): CodeNode[];
|
|
@@ -15,74 +15,107 @@ export const JS_BUILTINS = new Set([
|
|
|
15
15
|
function makeId(filePath, name) {
|
|
16
16
|
return `${filePath}::${name}`;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Only capture custom hooks — use[A-Z] pattern
|
|
40
|
-
// Built-in React hooks (useState etc.) are not in our node graph
|
|
41
|
-
if (/^use[A-Z]/.test(name))
|
|
42
|
-
hooks.push(name);
|
|
18
|
+
// Cheap arg-type inference without the type checker: literal/object/array/
|
|
19
|
+
// function/new expressions map to coarse type tags; identifiers and property
|
|
20
|
+
// accesses stay undefined (resolving those needs the checker, too slow for
|
|
21
|
+
// whole-repo analysis).
|
|
22
|
+
function inferArgType(arg) {
|
|
23
|
+
const kind = arg.getKind?.();
|
|
24
|
+
if (kind === SyntaxKind.StringLiteral || kind === SyntaxKind.TemplateExpression)
|
|
25
|
+
return "string";
|
|
26
|
+
if (kind === SyntaxKind.NumericLiteral)
|
|
27
|
+
return "number";
|
|
28
|
+
if (kind === SyntaxKind.TrueKeyword || kind === SyntaxKind.FalseKeyword)
|
|
29
|
+
return "boolean";
|
|
30
|
+
if (kind === SyntaxKind.ObjectLiteralExpression)
|
|
31
|
+
return "object";
|
|
32
|
+
if (kind === SyntaxKind.ArrayLiteralExpression)
|
|
33
|
+
return "array";
|
|
34
|
+
if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression)
|
|
35
|
+
return "function";
|
|
36
|
+
if (kind === SyntaxKind.NewExpression) {
|
|
37
|
+
const ident = arg.getExpression?.();
|
|
38
|
+
return typeof ident?.getName === "function" ? ident.getName() : undefined;
|
|
43
39
|
}
|
|
44
|
-
return
|
|
40
|
+
return undefined;
|
|
45
41
|
}
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// Arg-shape tags for the API-call families, used by the single-walk
|
|
43
|
+
// classifier below.
|
|
44
|
+
const API_HOOK_FAMILIES = new Set(["useQuery", "useMutation", "useInfiniteQuery", "useSuspenseQuery", "useSWR", "useSWRMutation"]);
|
|
45
|
+
const AXIOS_METHODS = new Set(["axios.get", "axios.post", "axios.put", "axios.delete", "axios.patch", "axios"]);
|
|
46
|
+
// SINGLE traversal over all call expressions producing every call-derived
|
|
47
|
+
// output at once: structured call sites + legacy bare names, custom hook
|
|
48
|
+
// calls, and API-call strings. The per-aspect extractors used to walk the
|
|
49
|
+
// same descendants tree four times per function-like; whole-repo cost is now
|
|
50
|
+
// one walk.
|
|
51
|
+
export function extractCallsWithSites(node) {
|
|
52
|
+
const callExprs = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
53
|
+
const sites = [];
|
|
54
|
+
const seenSites = new Set();
|
|
55
|
+
const hookCalls = [];
|
|
48
56
|
const apiCalls = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const args = call.getArguments();
|
|
66
|
-
if (args.length > 0)
|
|
67
|
-
apiCalls.push(`${expr}(${args[0].getText()})`);
|
|
57
|
+
const dependencyNames = [];
|
|
58
|
+
const contextRefs = [];
|
|
59
|
+
for (const call of callExprs) {
|
|
60
|
+
const name = call.getExpression().getText();
|
|
61
|
+
// use* family: custom hooks (^use[A-Z]) plus ALL use* names (incl.
|
|
62
|
+
// built-ins like useState) for hook dependency tracking
|
|
63
|
+
if (name.startsWith("use")) {
|
|
64
|
+
if (/^use[A-Z]/.test(name) && !hookCalls.includes(name))
|
|
65
|
+
hookCalls.push(name);
|
|
66
|
+
if (!dependencyNames.includes(name))
|
|
67
|
+
dependencyNames.push(name);
|
|
68
|
+
if (name === "useContext") {
|
|
69
|
+
const ctxArg = call.getArguments()[0];
|
|
70
|
+
if (ctxArg && !contextRefs.includes(ctxArg.getText()))
|
|
71
|
+
contextRefs.push(ctxArg.getText());
|
|
72
|
+
}
|
|
68
73
|
}
|
|
69
|
-
//
|
|
70
|
-
if (
|
|
71
|
-
expr === "useMutation" ||
|
|
72
|
-
expr === "useInfiniteQuery" ||
|
|
73
|
-
expr === "useSuspenseQuery") {
|
|
74
|
+
// API-call families: fetch / axios.* / React Query / SWR
|
|
75
|
+
if (name === "fetch" || AXIOS_METHODS.has(name) || API_HOOK_FAMILIES.has(name)) {
|
|
74
76
|
const args = call.getArguments();
|
|
75
77
|
if (args.length > 0)
|
|
76
|
-
apiCalls.push(`${
|
|
78
|
+
apiCalls.push(`${name}(${args[0].getText()})`);
|
|
77
79
|
}
|
|
78
|
-
//
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Plain calls/sites skip hooks and builtin noise
|
|
81
|
+
if (name.startsWith("use"))
|
|
82
|
+
continue;
|
|
83
|
+
const rootName = name.split(".")[0];
|
|
84
|
+
if (JS_BUILTINS.has(rootName))
|
|
85
|
+
continue;
|
|
86
|
+
const args = call.getArguments();
|
|
87
|
+
let hasSpread = false;
|
|
88
|
+
const argTypes = [];
|
|
89
|
+
for (const arg of args) {
|
|
90
|
+
if (arg.getKind?.() === SyntaxKind.SpreadElement) {
|
|
91
|
+
hasSpread = true;
|
|
92
|
+
argTypes.push("unknown");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
argTypes.push(inferArgType(arg) ?? "unknown");
|
|
96
|
+
}
|
|
83
97
|
}
|
|
98
|
+
// One record per (name, arity, argTypes) triple — two calls to the same
|
|
99
|
+
// name with different arg counts OR arg type shapes are both overload
|
|
100
|
+
// signals (same arity + different literal types can hit different
|
|
101
|
+
// overloads, and each deserves its own edge).
|
|
102
|
+
const key = `${name}/${args.length}/${argTypes.join(",")}`;
|
|
103
|
+
if (seenSites.has(key))
|
|
104
|
+
continue;
|
|
105
|
+
seenSites.add(key);
|
|
106
|
+
sites.push({ name, argCount: args.length, argTypes, ...(hasSpread && { hasSpread: true }) });
|
|
84
107
|
}
|
|
85
|
-
return
|
|
108
|
+
return {
|
|
109
|
+
calls: [...new Set(sites.map((cs) => cs.name))],
|
|
110
|
+
callSites: sites,
|
|
111
|
+
hookCalls,
|
|
112
|
+
apiCalls,
|
|
113
|
+
dependencyNames,
|
|
114
|
+
contextRefs,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function extractFunctionCalls(node) {
|
|
118
|
+
return extractCallsWithSites(node).calls;
|
|
86
119
|
}
|
|
87
120
|
export function hasErrorHandling(node) {
|
|
88
121
|
const tryCatch = node.getDescendantsOfKind(SyntaxKind.TryStatement);
|
|
@@ -109,9 +142,7 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
109
142
|
if (/^use[A-Z]/.test(name))
|
|
110
143
|
continue;
|
|
111
144
|
const typedParams = extractParams(fn);
|
|
112
|
-
const calls =
|
|
113
|
-
const hookCalls = extractHookCalls(fn);
|
|
114
|
-
const apiCalls = extractApiCalls(fn);
|
|
145
|
+
const { calls, callSites, hookCalls, apiCalls } = extractCallsWithSites(fn);
|
|
115
146
|
const isAsync = fn.isAsync();
|
|
116
147
|
const hasErrors = hasErrorHandling(fn);
|
|
117
148
|
const throws = extractThrowStatements(fn);
|
|
@@ -119,6 +150,9 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
119
150
|
const returnType = extractReturnTypeAnnotation(fn);
|
|
120
151
|
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
121
152
|
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
153
|
+
// Overload signatures have no body — flagged so the disambiguator can
|
|
154
|
+
// collapse them into the implementation when signatures match.
|
|
155
|
+
const isOverloadSignature = !fn.getBody();
|
|
122
156
|
nodes.push({
|
|
123
157
|
id: makeId(filePath, name),
|
|
124
158
|
name,
|
|
@@ -133,6 +167,8 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
133
167
|
returnType,
|
|
134
168
|
referencedTypes,
|
|
135
169
|
calls,
|
|
170
|
+
callSites,
|
|
171
|
+
...(isOverloadSignature && { isOverloadSignature: true }),
|
|
136
172
|
hookCalls,
|
|
137
173
|
apiCalls,
|
|
138
174
|
isAsync,
|
|
@@ -144,6 +180,40 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
144
180
|
...(renderingBoundary !== null && { renderingBoundary }),
|
|
145
181
|
},
|
|
146
182
|
});
|
|
183
|
+
// Overload signature declarations — ts-morph's getFunctions() returns only
|
|
184
|
+
// the implementation; signatures hang off getOverloads(). Each becomes its
|
|
185
|
+
// own FUNCTION node (no body) so the disambiguator can give it a
|
|
186
|
+
// signature-derived id or collapse it into an identical implementation.
|
|
187
|
+
for (const overload of fn.getOverloads() ?? []) {
|
|
188
|
+
const ovParams = extractParams(overload);
|
|
189
|
+
const ovReturnType = extractReturnTypeAnnotation(overload);
|
|
190
|
+
nodes.push({
|
|
191
|
+
id: makeId(filePath, name),
|
|
192
|
+
name,
|
|
193
|
+
type: "FUNCTION",
|
|
194
|
+
filePath,
|
|
195
|
+
startLine: overload.getStartLineNumber(),
|
|
196
|
+
endLine: overload.getEndLineNumber(),
|
|
197
|
+
rawCode: overload.getText(),
|
|
198
|
+
metadata: {
|
|
199
|
+
params: ovParams.map((p) => p.name),
|
|
200
|
+
parameters: ovParams,
|
|
201
|
+
returnType: ovReturnType,
|
|
202
|
+
referencedTypes: [],
|
|
203
|
+
calls: [],
|
|
204
|
+
callSites: [],
|
|
205
|
+
hookCalls: [],
|
|
206
|
+
apiCalls: [],
|
|
207
|
+
isAsync: false,
|
|
208
|
+
hasErrorHandling: false,
|
|
209
|
+
throws: false,
|
|
210
|
+
lineCount: overload.getEndLineNumber() - overload.getStartLineNumber(),
|
|
211
|
+
isHttpHandler: HTTP_METHOD_EXPORTS.has(name),
|
|
212
|
+
httpMethod: HTTP_METHOD_EXPORTS.has(name) ? name : undefined,
|
|
213
|
+
isOverloadSignature: true,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
147
217
|
}
|
|
148
218
|
// ─── Arrow Function Declarations ───────────────────────────────────────────
|
|
149
219
|
for (const variable of file.getVariableDeclarations()) {
|
|
@@ -161,9 +231,7 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
161
231
|
if (!isArrow)
|
|
162
232
|
continue;
|
|
163
233
|
const typedParams = extractParams(initializer);
|
|
164
|
-
const calls =
|
|
165
|
-
const hookCalls = extractHookCalls(initializer);
|
|
166
|
-
const apiCalls = extractApiCalls(initializer);
|
|
234
|
+
const { calls, callSites, hookCalls, apiCalls } = extractCallsWithSites(initializer);
|
|
167
235
|
const isAsync = initializer.getText().startsWith("async");
|
|
168
236
|
const hasErrors = hasErrorHandling(initializer);
|
|
169
237
|
const throws = extractThrowStatements(initializer);
|
|
@@ -185,6 +253,7 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
185
253
|
returnType,
|
|
186
254
|
referencedTypes,
|
|
187
255
|
calls,
|
|
256
|
+
callSites,
|
|
188
257
|
hookCalls,
|
|
189
258
|
apiCalls,
|
|
190
259
|
isAsync,
|
|
@@ -230,6 +299,7 @@ export function extractFunctions(file, fileDirective = null) {
|
|
|
230
299
|
metadata: {
|
|
231
300
|
params: [],
|
|
232
301
|
calls: [],
|
|
302
|
+
callSites: [],
|
|
233
303
|
apiCalls: [],
|
|
234
304
|
isAsync: false,
|
|
235
305
|
hasErrorHandling: false,
|