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.
Files changed (75) hide show
  1. package/dist/config/backcompat.test.d.ts +1 -0
  2. package/dist/config/backcompat.test.js +188 -0
  3. package/dist/config/index.d.ts +5 -3
  4. package/dist/config/index.js +37 -54
  5. package/dist/config/providers/file.d.ts +3 -1
  6. package/dist/config/providers/file.js +47 -53
  7. package/dist/config/providers/providers.default.js +0 -1
  8. package/dist/config/providers/request.js +1 -1
  9. package/dist/config/types.d.ts +0 -1
  10. package/dist/config/types.js +2 -18
  11. package/dist/graph/edges/callEdges.js +97 -5
  12. package/dist/graph/edges/callEdges.test.js +154 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/jobs/queue/memory.js +12 -1
  16. package/dist/parser/extractors/classes.js +23 -4
  17. package/dist/parser/extractors/functions.d.ts +10 -3
  18. package/dist/parser/extractors/functions.js +135 -65
  19. package/dist/parser/extractors/hooks.js +5 -29
  20. package/dist/parser/extractors/objectMethods.js +3 -4
  21. package/dist/parser/index.js +11 -6
  22. package/dist/parser/overloads.d.ts +2 -0
  23. package/dist/parser/overloads.js +96 -0
  24. package/dist/parser/overloads.test.d.ts +1 -0
  25. package/dist/parser/overloads.test.js +175 -0
  26. package/dist/parser/typeUtils.d.ts +2 -0
  27. package/dist/parser/typeUtils.js +2 -0
  28. package/dist/summarizer/providers/index.js +4 -4
  29. package/dist/summarizer/providers/openai.js +1 -1
  30. package/dist/types.d.ts +6 -0
  31. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  32. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  33. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  34. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  35. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  36. package/extractors/go/calls.go +134 -2
  37. package/extractors/go/contract.go +1 -0
  38. package/extractors/go/exports.go +107 -0
  39. package/extractors/go/exports_test.go +64 -0
  40. package/extractors/go/extractor.go +1 -0
  41. package/extractors/go/lookup.go +63 -7
  42. package/extractors/go/nodes.go +28 -0
  43. package/extractors/go/parser.go +51 -8
  44. package/extractors/java/devlens_java_extractor.jar +0 -0
  45. package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
  46. package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
  47. package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
  48. package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
  49. package/extractors/java/src/devlens/extractor/Parser.java +39 -0
  50. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
  51. package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
  52. package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
  53. package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
  54. package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
  55. package/extractors/python/devlens_extractors_python/extractor.py +7 -1
  56. package/extractors/python/devlens_extractors_python/lookup.py +6 -0
  57. package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
  58. package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
  59. package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
  60. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  61. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  62. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  63. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  64. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  65. package/extractors/rust/src/calls.rs +27 -16
  66. package/extractors/rust/src/contract.rs +2 -0
  67. package/extractors/rust/src/exports.rs +387 -0
  68. package/extractors/rust/src/exports_test.rs +105 -0
  69. package/extractors/rust/src/extractor.rs +2 -0
  70. package/extractors/rust/src/lookup.rs +60 -0
  71. package/extractors/rust/src/main.rs +4 -0
  72. package/extractors/rust/src/nodes.rs +19 -0
  73. package/extractors/rust/src/parser.rs +66 -1
  74. package/extractors/rust/src/routes.rs +2 -1
  75. 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
- export function extractFunctionCalls(node) {
19
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
20
- const names = [];
21
- for (const call of calls) {
22
- const name = call.getExpression().getText();
23
- // Skip hooks, they are handled by hooks extractor
24
- if (name.startsWith("use"))
25
- continue;
26
- // Skip console calls and other noise calls, they are noise
27
- const rootName = name.split(".")[0];
28
- if (JS_BUILTINS.has(rootName))
29
- continue;
30
- names.push(name);
31
- }
32
- return [...new Set(names)];
33
- }
34
- export function extractHookCalls(node) {
35
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
36
- const hooks = [];
37
- for (const call of calls) {
38
- const name = call.getExpression().getText();
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 [...new Set(hooks)];
40
+ return undefined;
45
41
  }
46
- export function extractApiCalls(node) {
47
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
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
- for (const call of calls) {
50
- const text = call.getText();
51
- const expr = call.getExpression().getText();
52
- // ─── fetch ────────────────────────────────────────────────────────────────
53
- if (expr === "fetch") {
54
- const args = call.getArguments();
55
- if (args.length > 0)
56
- apiCalls.push(`fetch(${args[0].getText()})`);
57
- }
58
- // ─── axios ────────────────────────────────────────────────────────────────
59
- if (expr === "axios.get" ||
60
- expr === "axios.post" ||
61
- expr === "axios.put" ||
62
- expr === "axios.delete" ||
63
- expr === "axios.patch" ||
64
- expr === "axios") {
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
- // ─── React Query (useQuery, useMutation, useInfiniteQuery) ────────────────
70
- if (expr === "useQuery" ||
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(`${expr}(${args[0].getText()})`);
78
+ apiCalls.push(`${name}(${args[0].getText()})`);
77
79
  }
78
- // ─── SWR ──────────────────────────────────────────────────────────────────
79
- if (expr === "useSWR" || expr === "useSWRMutation") {
80
- const args = call.getArguments();
81
- if (args.length > 0)
82
- apiCalls.push(`${expr}(${args[0].getText()})`);
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 [...new Set(apiCalls)];
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 = extractFunctionCalls(fn);
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 = extractFunctionCalls(initializer);
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 { extractFunctionCalls } from "./functions.js";
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 = extractDependencies(fn);
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 = extractDependencies(initializer);
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 { extractFunctionCalls, extractHookCalls, extractApiCalls, hasErrorHandling, extractThrowStatements, } from "./functions.js";
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 = extractFunctionCalls(fnNode);
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,
@@ -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 = extracted.map(n => n.name);
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 = extracted.map(n => n.rawCode ?? "").join("\n");
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 = extracted.length;
141
- fileNode.metadata.childNodeIds = extracted.map(n => n.id);
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 = extracted.map(n => n.rawCode ?? "").join("\n");
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, ...extracted);
152
+ allNodes.push(fileNode, ...overloads);
148
153
  }
149
154
  }
150
155
  catch (error) {
@@ -0,0 +1,2 @@
1
+ import type { CodeNode } from "../types.js";
2
+ export declare function applyOverloadDisambiguation(nodes: CodeNode[], relativePath: string): void;
@@ -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 {};