devlensio 1.0.3 → 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/config/backcompat.test.d.ts +1 -0
- package/dist/config/backcompat.test.js +188 -0
- package/dist/config/index.d.ts +5 -3
- package/dist/config/index.js +37 -54
- package/dist/config/providers/file.d.ts +3 -1
- package/dist/config/providers/file.js +47 -53
- package/dist/config/providers/providers.default.js +0 -1
- package/dist/config/providers/request.js +1 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/config/types.js +2 -18
- package/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jobs/queue/memory.js +12 -1
- 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/summarizer/providers/index.js +4 -4
- package/dist/summarizer/providers/openai.js +1 -1
- 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 +10 -3
|
@@ -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,
|
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
import { SyntaxKind } from "ts-morph";
|
|
2
2
|
import { detectFunctionDirective } from "../directives.js";
|
|
3
3
|
import { extractParams, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
4
|
-
import {
|
|
4
|
+
import { extractCallsWithSites } from "./functions.js";
|
|
5
5
|
function makeId(filePath, name) {
|
|
6
6
|
return `${filePath}::${name}`;
|
|
7
7
|
}
|
|
8
|
-
function extractDependencies(node) {
|
|
9
|
-
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
10
|
-
const deps = [];
|
|
11
|
-
for (const call of calls) {
|
|
12
|
-
const name = call.getExpression().getText();
|
|
13
|
-
if (name.startsWith("use")) {
|
|
14
|
-
deps.push(name);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return [...new Set(deps)];
|
|
18
|
-
}
|
|
19
|
-
function extractContextRefs(node) {
|
|
20
|
-
const refs = [];
|
|
21
|
-
for (const call of node.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
22
|
-
if (call.getExpression().getText() === "useContext") {
|
|
23
|
-
const arg = call.getArguments()[0];
|
|
24
|
-
if (arg)
|
|
25
|
-
refs.push(arg.getText());
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return [...new Set(refs)];
|
|
29
|
-
}
|
|
30
8
|
// Try explicit annotation first; fall back to shape heuristic.
|
|
31
9
|
function extractReturnType(node) {
|
|
32
10
|
const explicit = node.getReturnTypeNode?.()?.getText();
|
|
@@ -58,11 +36,9 @@ export function extractHooks(file, fileDirective = null) {
|
|
|
58
36
|
// Hooks must start with "use" followed by uppercase
|
|
59
37
|
if (!/^use[A-Z]/.test(name))
|
|
60
38
|
continue;
|
|
61
|
-
const dependencies =
|
|
62
|
-
const calls = extractFunctionCalls(fn);
|
|
39
|
+
const { calls, callSites, dependencyNames: dependencies, contextRefs } = extractCallsWithSites(fn);
|
|
63
40
|
const returnType = extractReturnType(fn);
|
|
64
41
|
const isAsync = fn.isAsync();
|
|
65
|
-
const contextRefs = extractContextRefs(fn);
|
|
66
42
|
const renderingBoundary = detectFunctionDirective(fn.getBody()) ?? fileDirective;
|
|
67
43
|
const typedParams = extractParams(fn);
|
|
68
44
|
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
@@ -78,6 +54,7 @@ export function extractHooks(file, fileDirective = null) {
|
|
|
78
54
|
metadata: {
|
|
79
55
|
dependencies,
|
|
80
56
|
calls,
|
|
57
|
+
callSites,
|
|
81
58
|
contextRefs,
|
|
82
59
|
returnType,
|
|
83
60
|
parameters: typedParams,
|
|
@@ -100,11 +77,9 @@ export function extractHooks(file, fileDirective = null) {
|
|
|
100
77
|
const isArrow = initializer.getKind() === SyntaxKind.ArrowFunction;
|
|
101
78
|
if (!isArrow)
|
|
102
79
|
continue;
|
|
103
|
-
const dependencies =
|
|
104
|
-
const calls = extractFunctionCalls(initializer);
|
|
80
|
+
const { calls, callSites, dependencyNames: dependencies, contextRefs } = extractCallsWithSites(initializer);
|
|
105
81
|
const returnType = extractReturnType(initializer);
|
|
106
82
|
const isAsync = initializer.asKind(SyntaxKind.ArrowFunction)?.isAsync() ?? false;
|
|
107
|
-
const contextRefs = extractContextRefs(initializer);
|
|
108
83
|
const renderingBoundary = detectFunctionDirective(initializer.getBody?.()) ?? fileDirective;
|
|
109
84
|
const typedParams = extractParams(initializer);
|
|
110
85
|
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
@@ -120,6 +95,7 @@ export function extractHooks(file, fileDirective = null) {
|
|
|
120
95
|
metadata: {
|
|
121
96
|
dependencies,
|
|
122
97
|
calls,
|
|
98
|
+
callSites,
|
|
123
99
|
contextRefs,
|
|
124
100
|
returnType,
|
|
125
101
|
parameters: typedParams,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SyntaxKind } from "ts-morph";
|
|
2
2
|
import { detectFunctionDirective } from "../directives.js";
|
|
3
|
-
import {
|
|
3
|
+
import { extractCallsWithSites, hasErrorHandling, extractThrowStatements, } from "./functions.js";
|
|
4
4
|
import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
5
5
|
// Extracts function-valued properties of object literals as FUNCTION nodes.
|
|
6
6
|
//
|
|
@@ -42,9 +42,7 @@ function isAsyncFn(fnNode) {
|
|
|
42
42
|
function buildNode(file, dottedName, fnNode, spanNode, fileDirective) {
|
|
43
43
|
const filePath = file.getFilePath();
|
|
44
44
|
const typedParams = extractParams(fnNode);
|
|
45
|
-
const calls =
|
|
46
|
-
const hookCalls = extractHookCalls(fnNode);
|
|
47
|
-
const apiCalls = extractApiCalls(fnNode);
|
|
45
|
+
const { calls, callSites, hookCalls, apiCalls } = extractCallsWithSites(fnNode);
|
|
48
46
|
const isAsync = isAsyncFn(fnNode);
|
|
49
47
|
const hasErrors = hasErrorHandling(fnNode);
|
|
50
48
|
const throws = extractThrowStatements(fnNode);
|
|
@@ -66,6 +64,7 @@ function buildNode(file, dottedName, fnNode, spanNode, fileDirective) {
|
|
|
66
64
|
returnType,
|
|
67
65
|
referencedTypes,
|
|
68
66
|
calls,
|
|
67
|
+
callSites,
|
|
69
68
|
hookCalls,
|
|
70
69
|
apiCalls,
|
|
71
70
|
isAsync,
|
package/dist/parser/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { extractStores } from "./extractors/stores.js";
|
|
|
8
8
|
import { extractObjectMethods } from "./extractors/objectMethods.js";
|
|
9
9
|
import { extractClasses } from "./extractors/classes.js";
|
|
10
10
|
import { detectFileDirective } from "./directives.js";
|
|
11
|
+
import { applyOverloadDisambiguation } from "./overloads.js";
|
|
11
12
|
import { createHash } from "crypto";
|
|
12
13
|
// Directories to skip entirely while walking
|
|
13
14
|
const IGNORE_DIRS = [
|
|
@@ -123,13 +124,17 @@ export function parseRepo(repoPath) {
|
|
|
123
124
|
node.codeHash = createHash("sha256").update(node.rawCode).digest("hex").slice(0, 16);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
// Same-name function-likes in one file (overloads) get arity/signature
|
|
128
|
+
// discriminator suffixes; signature-identical declarations collapse.
|
|
129
|
+
applyOverloadDisambiguation(extracted, relativePath);
|
|
130
|
+
const overloads = extracted.filter((n) => !n.metadata.isDuplicateOverload);
|
|
126
131
|
if (fileType === "TEST" || fileType === "STORY") {
|
|
127
132
|
// Do not add child nodes for test/story files — they are test helpers,
|
|
128
|
-
fileNode.metadata.testCases =
|
|
133
|
+
fileNode.metadata.testCases = overloads.map(n => n.name);
|
|
129
134
|
fileNode.metadata.nodeCount = 0;
|
|
130
135
|
fileNode.metadata.childNodeIds = [];
|
|
131
136
|
// File hash based on all child code combined
|
|
132
|
-
const fileRawCode =
|
|
137
|
+
const fileRawCode = overloads.map(n => n.rawCode ?? "").join("\n");
|
|
133
138
|
if (fileRawCode.trim()) {
|
|
134
139
|
fileNode.codeHash = createHash("sha256")
|
|
135
140
|
.update(fileRawCode).digest("hex").slice(0, 16);
|
|
@@ -137,14 +142,14 @@ export function parseRepo(repoPath) {
|
|
|
137
142
|
allNodes.push(fileNode); // only the file node, no children
|
|
138
143
|
}
|
|
139
144
|
else {
|
|
140
|
-
fileNode.metadata.nodeCount =
|
|
141
|
-
fileNode.metadata.childNodeIds =
|
|
145
|
+
fileNode.metadata.nodeCount = overloads.length;
|
|
146
|
+
fileNode.metadata.childNodeIds = overloads.map(n => n.id);
|
|
142
147
|
// File node hash — based on all child code combined
|
|
143
|
-
const fileRawCode =
|
|
148
|
+
const fileRawCode = overloads.map(n => n.rawCode ?? "").join("\n");
|
|
144
149
|
if (fileRawCode.trim()) {
|
|
145
150
|
fileNode.codeHash = createHash("sha256").update(fileRawCode).digest("hex").slice(0, 16);
|
|
146
151
|
}
|
|
147
|
-
allNodes.push(fileNode, ...
|
|
152
|
+
allNodes.push(fileNode, ...overloads);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
catch (error) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Overload disambiguation — same-name function-likes in the same file.
|
|
2
|
+
//
|
|
3
|
+
// Node ids are `${relativePath}::${name}`, so a file with two `formatValue`
|
|
4
|
+
// functions (or an overloaded `Logger.log` method pair) produced two nodes
|
|
5
|
+
// with ONE id; downstream dedupe (buildGraphResponse, summarizer, storage)
|
|
6
|
+
// silently dropped all but the first and every CALLS edge picked arbitrarily.
|
|
7
|
+
//
|
|
8
|
+
// Convention (mirrored by the Go/Java/Python/Rust extractors — keep in sync,
|
|
9
|
+
// see MULTI_LANGUAGE_EXPANSION.md):
|
|
10
|
+
// - Unique name in file → id unchanged (`utils.ts::formatValue`)
|
|
11
|
+
// - Collision, distinct arities → `#arity` suffix (`utils.ts::formatValue#2`)
|
|
12
|
+
// - Collision, same arity, different param types → `#arity#<sig8>` where
|
|
13
|
+
// sig8 = first 8 hex of sha256 over the normalized param-type list
|
|
14
|
+
// - Arity-suffix never uses ordinals: the suffix is the declaration's own
|
|
15
|
+
// parameter count, so reordering functions in a file cannot change ids
|
|
16
|
+
//
|
|
17
|
+
// Signature-identical declarations (a TS overload signature plus an
|
|
18
|
+
// implementation with the same param list) collapse into ONE node, keeping
|
|
19
|
+
// the one with a body — they are the same callable.
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
const OVERLOADABLE_TYPES = new Set(["FUNCTION", "METHOD"]);
|
|
22
|
+
function paramTypes(node) {
|
|
23
|
+
const parameters = node.metadata?.parameters;
|
|
24
|
+
if (!Array.isArray(parameters))
|
|
25
|
+
return [];
|
|
26
|
+
return parameters.map((p) => {
|
|
27
|
+
const t = typeof p?.type === "string" && p.type.length > 0 ? p.type : "unknown";
|
|
28
|
+
if (p?.isRest)
|
|
29
|
+
return `...${t}`;
|
|
30
|
+
if (p?.isOptional)
|
|
31
|
+
return `${t}?`;
|
|
32
|
+
return t;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function signatureKey(node) {
|
|
36
|
+
return `(${paramTypes(node).join(",")})`;
|
|
37
|
+
}
|
|
38
|
+
function hasBody(node) {
|
|
39
|
+
return node.metadata?.isOverloadSignature !== true;
|
|
40
|
+
}
|
|
41
|
+
export function applyOverloadDisambiguation(nodes, relativePath) {
|
|
42
|
+
// Group function-likes by name (METHOD names are dotted `Class.method`, so
|
|
43
|
+
// same-name methods on different classes never collide here).
|
|
44
|
+
const groups = new Map();
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
if (!OVERLOADABLE_TYPES.has(node.type))
|
|
47
|
+
continue;
|
|
48
|
+
if (!groups.has(node.name))
|
|
49
|
+
groups.set(node.name, []);
|
|
50
|
+
groups.get(node.name).push(node);
|
|
51
|
+
}
|
|
52
|
+
for (const [name, group] of groups) {
|
|
53
|
+
if (group.length < 2)
|
|
54
|
+
continue;
|
|
55
|
+
group.sort((a, b) => a.startLine - b.startLine);
|
|
56
|
+
// Collapse signature-identical declarations, preferring the one with a body.
|
|
57
|
+
// Single pass — each node's signature key is computed exactly once.
|
|
58
|
+
const bySignature = new Map();
|
|
59
|
+
const keys = new Map();
|
|
60
|
+
for (const node of group) {
|
|
61
|
+
const key = signatureKey(node);
|
|
62
|
+
keys.set(node, key);
|
|
63
|
+
const existing = bySignature.get(key);
|
|
64
|
+
if (!existing) {
|
|
65
|
+
bySignature.set(key, node);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Keep the implementation; drop the bare signature duplicate.
|
|
69
|
+
if (!hasBody(existing) && hasBody(node))
|
|
70
|
+
bySignature.set(key, node);
|
|
71
|
+
}
|
|
72
|
+
const survivors = [...bySignature.values()].sort((a, b) => a.startLine - b.startLine);
|
|
73
|
+
if (survivors.length < group.length) {
|
|
74
|
+
const kept = new Set(survivors);
|
|
75
|
+
for (const node of group) {
|
|
76
|
+
if (!kept.has(node))
|
|
77
|
+
node.metadata.isDuplicateOverload = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Single survivor after collapse → the name is unique again, no suffix.
|
|
81
|
+
if (survivors.length < 2)
|
|
82
|
+
continue;
|
|
83
|
+
// Distinct arities within the group → readable `#arity` suffix.
|
|
84
|
+
const arities = new Set(survivors.map((n) => n.metadata?.params?.length ?? 0));
|
|
85
|
+
const useArityOnly = arities.size === survivors.length;
|
|
86
|
+
for (const node of survivors) {
|
|
87
|
+
const arity = node.metadata?.params?.length ?? 0;
|
|
88
|
+
let suffix = `#${arity}`;
|
|
89
|
+
if (!useArityOnly) {
|
|
90
|
+
const sig8 = createHash("sha256").update(keys.get(node)).digest("hex").slice(0, 8);
|
|
91
|
+
suffix = `#${arity}#${sig8}`;
|
|
92
|
+
}
|
|
93
|
+
node.id = `${relativePath}::${name}${suffix}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|