@optave/codegraph 3.11.2 → 3.12.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 (167) hide show
  1. package/README.md +8 -8
  2. package/dist/db/migrations.d.ts.map +1 -1
  3. package/dist/db/migrations.js +7 -0
  4. package/dist/db/migrations.js.map +1 -1
  5. package/dist/domain/analysis/module-map.d.ts +2 -0
  6. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  7. package/dist/domain/analysis/module-map.js +24 -2
  8. package/dist/domain/analysis/module-map.js.map +1 -1
  9. package/dist/domain/graph/builder/call-resolver.d.ts +4 -2
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  11. package/dist/domain/graph/builder/call-resolver.js +170 -8
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  13. package/dist/domain/graph/builder/cha.d.ts +61 -0
  14. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  15. package/dist/domain/graph/builder/cha.js +143 -0
  16. package/dist/domain/graph/builder/cha.js.map +1 -0
  17. package/dist/domain/graph/builder/context.d.ts +3 -0
  18. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  19. package/dist/domain/graph/builder/context.js +2 -0
  20. package/dist/domain/graph/builder/context.js.map +1 -1
  21. package/dist/domain/graph/builder/helpers.d.ts +17 -1
  22. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/helpers.js +159 -5
  24. package/dist/domain/graph/builder/helpers.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/incremental.js +73 -1
  27. package/dist/domain/graph/builder/incremental.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
  29. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/stages/build-edges.js +926 -26
  31. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  32. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  34. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  35. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
  37. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  38. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  39. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
  41. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  42. package/dist/domain/graph/journal.js +1 -1
  43. package/dist/domain/graph/journal.js.map +1 -1
  44. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  45. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  46. package/dist/domain/graph/resolver/points-to.js +213 -0
  47. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  48. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  49. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  50. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  51. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  52. package/dist/domain/parser.d.ts +10 -1
  53. package/dist/domain/parser.d.ts.map +1 -1
  54. package/dist/domain/parser.js +39 -7
  55. package/dist/domain/parser.js.map +1 -1
  56. package/dist/domain/wasm-worker-entry.js +25 -0
  57. package/dist/domain/wasm-worker-entry.js.map +1 -1
  58. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  59. package/dist/domain/wasm-worker-pool.js +32 -0
  60. package/dist/domain/wasm-worker-pool.js.map +1 -1
  61. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  62. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  63. package/dist/extractors/c.js +3 -3
  64. package/dist/extractors/c.js.map +1 -1
  65. package/dist/extractors/clojure.js +1 -1
  66. package/dist/extractors/clojure.js.map +1 -1
  67. package/dist/extractors/cpp.js +3 -3
  68. package/dist/extractors/cpp.js.map +1 -1
  69. package/dist/extractors/csharp.d.ts.map +1 -1
  70. package/dist/extractors/csharp.js +37 -8
  71. package/dist/extractors/csharp.js.map +1 -1
  72. package/dist/extractors/cuda.js +3 -3
  73. package/dist/extractors/cuda.js.map +1 -1
  74. package/dist/extractors/elixir.js +6 -6
  75. package/dist/extractors/elixir.js.map +1 -1
  76. package/dist/extractors/fsharp.js +1 -1
  77. package/dist/extractors/fsharp.js.map +1 -1
  78. package/dist/extractors/go.js +5 -5
  79. package/dist/extractors/go.js.map +1 -1
  80. package/dist/extractors/haskell.js +1 -1
  81. package/dist/extractors/haskell.js.map +1 -1
  82. package/dist/extractors/java.js +2 -2
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.d.ts +2 -0
  85. package/dist/extractors/javascript.d.ts.map +1 -1
  86. package/dist/extractors/javascript.js +1674 -64
  87. package/dist/extractors/javascript.js.map +1 -1
  88. package/dist/extractors/kotlin.js +5 -5
  89. package/dist/extractors/kotlin.js.map +1 -1
  90. package/dist/extractors/lua.js +1 -1
  91. package/dist/extractors/lua.js.map +1 -1
  92. package/dist/extractors/objc.js +3 -3
  93. package/dist/extractors/objc.js.map +1 -1
  94. package/dist/extractors/ocaml.js +1 -1
  95. package/dist/extractors/ocaml.js.map +1 -1
  96. package/dist/extractors/php.js +2 -2
  97. package/dist/extractors/php.js.map +1 -1
  98. package/dist/extractors/python.js +7 -7
  99. package/dist/extractors/python.js.map +1 -1
  100. package/dist/extractors/ruby.js +2 -2
  101. package/dist/extractors/ruby.js.map +1 -1
  102. package/dist/extractors/scala.js +1 -1
  103. package/dist/extractors/scala.js.map +1 -1
  104. package/dist/extractors/solidity.js +1 -1
  105. package/dist/extractors/solidity.js.map +1 -1
  106. package/dist/extractors/swift.js +4 -4
  107. package/dist/extractors/swift.js.map +1 -1
  108. package/dist/extractors/zig.js +4 -4
  109. package/dist/extractors/zig.js.map +1 -1
  110. package/dist/infrastructure/config.d.ts +10 -0
  111. package/dist/infrastructure/config.d.ts.map +1 -1
  112. package/dist/infrastructure/config.js +15 -0
  113. package/dist/infrastructure/config.js.map +1 -1
  114. package/dist/infrastructure/native.d.ts +11 -0
  115. package/dist/infrastructure/native.d.ts.map +1 -1
  116. package/dist/infrastructure/native.js +78 -5
  117. package/dist/infrastructure/native.js.map +1 -1
  118. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  119. package/dist/presentation/queries-cli/overview.js +5 -0
  120. package/dist/presentation/queries-cli/overview.js.map +1 -1
  121. package/dist/types.d.ts +184 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/package.json +7 -7
  124. package/src/db/migrations.ts +7 -0
  125. package/src/domain/analysis/module-map.ts +29 -1
  126. package/src/domain/graph/builder/call-resolver.ts +177 -7
  127. package/src/domain/graph/builder/cha.ts +175 -0
  128. package/src/domain/graph/builder/context.ts +3 -0
  129. package/src/domain/graph/builder/helpers.ts +175 -5
  130. package/src/domain/graph/builder/incremental.ts +79 -1
  131. package/src/domain/graph/builder/stages/build-edges.ts +1128 -24
  132. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  133. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  134. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  135. package/src/domain/graph/journal.ts +1 -1
  136. package/src/domain/graph/resolver/points-to.ts +254 -0
  137. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  138. package/src/domain/parser.ts +43 -5
  139. package/src/domain/wasm-worker-entry.ts +25 -0
  140. package/src/domain/wasm-worker-pool.ts +21 -0
  141. package/src/domain/wasm-worker-protocol.ts +14 -0
  142. package/src/extractors/c.ts +3 -3
  143. package/src/extractors/clojure.ts +1 -1
  144. package/src/extractors/cpp.ts +3 -3
  145. package/src/extractors/csharp.ts +33 -9
  146. package/src/extractors/cuda.ts +3 -3
  147. package/src/extractors/elixir.ts +6 -6
  148. package/src/extractors/fsharp.ts +1 -1
  149. package/src/extractors/go.ts +5 -5
  150. package/src/extractors/haskell.ts +1 -1
  151. package/src/extractors/java.ts +2 -2
  152. package/src/extractors/javascript.ts +1802 -66
  153. package/src/extractors/kotlin.ts +5 -5
  154. package/src/extractors/lua.ts +1 -1
  155. package/src/extractors/objc.ts +3 -3
  156. package/src/extractors/ocaml.ts +1 -1
  157. package/src/extractors/php.ts +2 -2
  158. package/src/extractors/python.ts +7 -7
  159. package/src/extractors/ruby.ts +2 -2
  160. package/src/extractors/scala.ts +1 -1
  161. package/src/extractors/solidity.ts +1 -1
  162. package/src/extractors/swift.ts +4 -4
  163. package/src/extractors/zig.ts +4 -4
  164. package/src/infrastructure/config.ts +15 -0
  165. package/src/infrastructure/native.ts +87 -5
  166. package/src/presentation/queries-cli/overview.ts +15 -1
  167. package/src/types.ts +194 -0
@@ -23,6 +23,28 @@ export interface CallNodeLookup {
23
23
 
24
24
  export const RECEIVER_KINDS = new Set(['class', 'struct', 'interface', 'type', 'module']);
25
25
 
26
+ /**
27
+ * Languages where bare `foo()` calls inside a class method are lexically scoped
28
+ * to the module, not the class — there is no implicit this/class binding.
29
+ * For these languages, the same-class fallback must not run for bare (no-receiver)
30
+ * calls that found no exact same-file match.
31
+ */
32
+ const MODULE_SCOPED_BARE_CALL_EXTENSIONS = new Set([
33
+ '.js',
34
+ '.mjs',
35
+ '.cjs',
36
+ '.jsx',
37
+ '.ts',
38
+ '.tsx',
39
+ '.mts',
40
+ '.cts',
41
+ ]);
42
+
43
+ export function isModuleScopedLanguage(relPath: string): boolean {
44
+ const ext = relPath.slice(relPath.lastIndexOf('.'));
45
+ return MODULE_SCOPED_BARE_CALL_EXTENSIONS.has(ext);
46
+ }
47
+
26
48
  // ── Shared resolution functions ──────────────────────────────────────────
27
49
 
28
50
  export function findCaller(
@@ -36,8 +58,9 @@ export function findCaller(
36
58
  }>,
37
59
  relPath: string,
38
60
  fileNodeRow: { id: number },
39
- ): { id: number } {
61
+ ): { id: number; callerName: string | null } {
40
62
  let caller: { id: number } | null = null;
63
+ let callerName: string | null = null;
41
64
  let callerSpan = Infinity;
42
65
  for (const def of definitions) {
43
66
  if (def.line <= call.line) {
@@ -48,13 +71,14 @@ export function findCaller(
48
71
  const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
49
72
  if (row) {
50
73
  caller = row;
74
+ callerName = def.name;
51
75
  callerSpan = span;
52
76
  }
53
77
  }
54
78
  }
55
79
  }
56
80
  }
57
- return caller ?? fileNodeRow;
81
+ return { ...(caller ?? fileNodeRow), callerName };
58
82
  }
59
83
 
60
84
  export function resolveByMethodOrGlobal(
@@ -62,17 +86,101 @@ export function resolveByMethodOrGlobal(
62
86
  call: { name: string; receiver?: string | null },
63
87
  relPath: string,
64
88
  typeMap: Map<string, unknown>,
89
+ callerName?: string | null,
65
90
  ): ReadonlyArray<{ id: number; file: string }> {
66
91
  if (call.receiver) {
67
- const typeEntry = typeMap.get(call.receiver);
68
- const typeName = typeEntry
92
+ // Strip "this." so `this.repo.method()` resolves via typeMap["repo"]
93
+ // (or the "this.repo" key seeded directly by the TSC property-declaration enricher).
94
+ const effectiveReceiver = call.receiver.startsWith('this.')
95
+ ? call.receiver.slice('this.'.length)
96
+ : call.receiver;
97
+ // For this.prop receivers, also try the class-scoped key (ClassName.prop) seeded by
98
+ // handlePropWriteTypeMap — prevents false edges when multiple classes define the same
99
+ // property name (issue #1323).
100
+ let typeEntry =
101
+ typeMap.get(effectiveReceiver) ??
102
+ typeMap.get(call.receiver) ??
103
+ // Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
104
+ // same-name rest-binding collision across functions in the same file (#1358).
105
+ (callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
106
+ if (!typeEntry && call.receiver.startsWith('this.') && callerName) {
107
+ const dotIdx = callerName.lastIndexOf('.');
108
+ if (dotIdx > -1) {
109
+ const callerClass = callerName.slice(0, dotIdx);
110
+ typeEntry = typeMap.get(`${callerClass}.${effectiveReceiver}`);
111
+ }
112
+ }
113
+ let typeName = typeEntry
69
114
  ? typeof typeEntry === 'string'
70
115
  ? typeEntry
71
116
  : (typeEntry as { type?: string }).type
72
117
  : null;
118
+
119
+ // Belt-and-suspenders fallback for inline new-expression receivers that
120
+ // extractReceiverName did not normalise (e.g. raw text leaked from an
121
+ // unhandled AST node type). extractReceiverName already handles the common
122
+ // `new_expression` / `parenthesized_expression(new_expression)` shapes by
123
+ // returning the constructor name directly, so this branch is exercised only
124
+ // by future node types or constructs that fall through to the raw-text path.
125
+ // The uppercase-initial restriction ([A-Z_$]) is a heuristic to distinguish
126
+ // constructors (PascalCase) from regular functions and avoids false positives
127
+ // on `(new xmlParser()).parse()` style calls.
128
+ if (!typeName && call.receiver) {
129
+ const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
130
+ if (m?.[1]) typeName = m[1];
131
+ }
132
+
73
133
  if (typeName) {
74
- const typed = lookup.byName(`${typeName}.${call.name}`).filter((n) => n.kind === 'method');
134
+ const typed = lookup
135
+ .byName(`${typeName}.${call.name}`)
136
+ .filter((n) => n.kind === 'method' && computeConfidence(relPath, n.file, null) >= 0.5);
75
137
  if (typed.length > 0) return typed;
138
+
139
+ // Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
140
+ // Checked after the symbol-DB lookup so an actual method definition always wins.
141
+ const protoEntry = typeMap.get(`${typeName}.${call.name}`);
142
+ const protoTarget = protoEntry
143
+ ? typeof protoEntry === 'string'
144
+ ? protoEntry
145
+ : (protoEntry as { type?: string }).type
146
+ : null;
147
+ if (protoTarget) {
148
+ const resolved = lookup
149
+ .byName(protoTarget)
150
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
151
+ if (resolved.length > 0) return resolved;
152
+ }
153
+ }
154
+
155
+ // Direct qualified method lookup: ClassName.staticMethod() or ClassName.instanceMethod()
156
+ // when the receiver is a class name with no typeMap entry. Handles static method calls
157
+ // like `C6.staticMethod()` or `D.d()` where the receiver IS the class.
158
+ // Matches both 'method' and 'function' kinds to cover field-initializer synthetic defs.
159
+ if (!typeName) {
160
+ const qualifiedName = `${effectiveReceiver}.${call.name}`;
161
+ const direct = lookup
162
+ .byName(qualifiedName)
163
+ .filter(
164
+ (n) =>
165
+ (n.kind === 'method' || n.kind === 'function') &&
166
+ computeConfidence(relPath, n.file, null) >= 0.5,
167
+ );
168
+ if (direct.length > 0) return direct;
169
+ }
170
+
171
+ // Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
172
+ // When a call site references `obj.prop` as a callback, resolve directly to the target fn.
173
+ const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);
174
+ const ptsTarget = compositeEntry
175
+ ? typeof compositeEntry === 'string'
176
+ ? compositeEntry
177
+ : (compositeEntry as { type?: string }).type
178
+ : null;
179
+ if (ptsTarget) {
180
+ const resolved = lookup
181
+ .byName(ptsTarget)
182
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
183
+ if (resolved.length > 0) return resolved;
76
184
  }
77
185
  }
78
186
  if (
@@ -81,7 +189,68 @@ export function resolveByMethodOrGlobal(
81
189
  call.receiver === 'self' ||
82
190
  call.receiver === 'super'
83
191
  ) {
84
- return lookup.byName(call.name).filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
192
+ // Phase 8.3f: accessor this-dispatch via Object.defineProperty.
193
+ // When a plain function (no class prefix) is registered as a get/set accessor for `obj`
194
+ // via Object.defineProperty, typeMap seeds 'callerName:this' = 'obj'.
195
+ // We then resolve this.method() → typeMap['obj.method'] → the concrete definition.
196
+ // This runs before the broad exact-name lookup to avoid false positives from
197
+ // unrelated same-file definitions.
198
+ if (call.receiver === 'this' && callerName && !callerName.includes('.')) {
199
+ const accessorThisEntry = typeMap.get(`${callerName}:this`);
200
+ const objName = accessorThisEntry
201
+ ? typeof accessorThisEntry === 'string'
202
+ ? accessorThisEntry
203
+ : (accessorThisEntry as { type?: string }).type
204
+ : null;
205
+ if (objName) {
206
+ const objMethodEntry = typeMap.get(`${objName}.${call.name}`);
207
+ const targetFn = objMethodEntry
208
+ ? typeof objMethodEntry === 'string'
209
+ ? objMethodEntry
210
+ : (objMethodEntry as { type?: string }).type
211
+ : null;
212
+ if (targetFn) {
213
+ const resolved = lookup
214
+ .byName(targetFn)
215
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
216
+ if (resolved.length > 0) return resolved;
217
+ }
218
+ }
219
+ }
220
+
221
+ const exact = lookup
222
+ .byName(call.name)
223
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
224
+ if (exact.length > 0) return exact;
225
+
226
+ // Try same-class method lookup via callerName.
227
+ // e.g. `this.area()` inside `Shape.describe` → try `Shape.area`.
228
+ // Also covers no-receiver calls inside class methods, e.g. `IsValidEmail(x)` inside
229
+ // `Validators.ValidateUser` → try `Validators.IsValidEmail` (C#/Java static siblings).
230
+ // This seeds the initial edge that runChaPostPass later expands to subclass overrides.
231
+ //
232
+ // For JS/TS, bare (no-receiver) calls are module-scoped — there is no implicit class
233
+ // binding. Skip the same-class fallback for bare calls in those languages to prevent
234
+ // false positives (e.g. `flush()` inside `Processor.run` must not resolve to
235
+ // `Processor.flush`). this.method() calls are unaffected: they still reach the fallback
236
+ // because `call.receiver === 'this'` is truthy, not a bare call.
237
+ const isBareCall = !call.receiver;
238
+ if (callerName && !(isBareCall && isModuleScopedLanguage(relPath))) {
239
+ const dotIdx = callerName.lastIndexOf('.');
240
+ if (dotIdx > -1) {
241
+ // Extract only the segment immediately before the method name so that
242
+ // 'Namespace.ClassName.method' yields 'ClassName', not 'Namespace.ClassName'.
243
+ // Symbols are stored under their bare class name, not their qualified path.
244
+ const prevDot = callerName.lastIndexOf('.', dotIdx - 1);
245
+ const callerClass = callerName.slice(prevDot + 1, dotIdx);
246
+ const qualifiedName = `${callerClass}.${call.name}`;
247
+ const sameClass = lookup
248
+ .byName(qualifiedName)
249
+ .filter((t) => t.kind === 'method' && computeConfidence(relPath, t.file, null) >= 0.5);
250
+ if (sameClass.length > 0) return sameClass;
251
+ }
252
+ }
253
+ return exact; // empty
85
254
  }
86
255
  return [];
87
256
  }
@@ -92,6 +261,7 @@ export function resolveCallTargets(
92
261
  relPath: string,
93
262
  importedNames: Map<string, string>,
94
263
  typeMap: Map<string, unknown>,
264
+ callerName?: string | null,
95
265
  ): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
96
266
  const importedFrom = importedNames.get(call.name);
97
267
  let targets: ReadonlyArray<{ id: number; file: string }> | undefined;
@@ -109,7 +279,7 @@ export function resolveCallTargets(
109
279
  if (!targets || targets.length === 0) {
110
280
  targets = lookup.byNameAndFile(call.name, relPath);
111
281
  if (targets.length === 0) {
112
- targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap);
282
+ targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap, callerName);
113
283
  }
114
284
  }
115
285
 
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Phase 8.5: Class Hierarchy Analysis (CHA) + Rapid Type Analysis (RTA)
3
+ *
4
+ * CHA resolves virtual/interface method dispatch to all known concrete
5
+ * implementations. RTA refines the CHA set by filtering out types that are
6
+ * never instantiated in the program (no `new X()` anywhere in the codebase).
7
+ *
8
+ * Used by:
9
+ * - buildFileCallEdges (WASM/JS path) — inline during per-file edge building
10
+ * - buildChaPostPass (native path) — JS post-pass on top of native edges
11
+ */
12
+
13
+ import type { ExtractorOutput } from '../../../types.js';
14
+ import type { CallNodeLookup } from './call-resolver.js';
15
+
16
+ // ── CHA context ──────────────────────────────────────────────────────────────
17
+
18
+ export interface ChaContext {
19
+ /** interface/class name → concrete classes that implement or extend it */
20
+ readonly implementors: ReadonlyMap<string, readonly string[]>;
21
+ /** class name → direct parent class name (from `extends`) */
22
+ readonly parents: ReadonlyMap<string, string>;
23
+ /** RTA: class names that appear in `new X()` anywhere in the project */
24
+ readonly instantiatedTypes: ReadonlySet<string>;
25
+ }
26
+
27
+ export const EMPTY_CHA_CONTEXT: ChaContext = {
28
+ implementors: new Map(),
29
+ parents: new Map(),
30
+ instantiatedTypes: new Set(),
31
+ };
32
+
33
+ /**
34
+ * Build the CHA context from all parsed file symbols.
35
+ *
36
+ * Must be called AFTER cross-file return-type propagation so that typeMap
37
+ * confidence values reflect propagated types (used for RTA seeding).
38
+ */
39
+ export function buildChaContext(fileSymbols: ReadonlyMap<string, ExtractorOutput>): ChaContext {
40
+ const implementors = new Map<string, string[]>();
41
+ const parents = new Map<string, string>();
42
+ const instantiatedTypes = new Set<string>();
43
+
44
+ for (const symbols of fileSymbols.values()) {
45
+ for (const cls of symbols.classes) {
46
+ if (cls.implements) {
47
+ let list = implementors.get(cls.implements);
48
+ if (!list) {
49
+ list = [];
50
+ implementors.set(cls.implements, list);
51
+ }
52
+ if (!list.includes(cls.name)) list.push(cls.name);
53
+ }
54
+ if (cls.extends) {
55
+ // child → parent (for this/super hierarchy walking)
56
+ if (!parents.has(cls.name)) parents.set(cls.name, cls.extends);
57
+ // parent → children (for CHA dispatch expansion via extends)
58
+ let list = implementors.get(cls.extends);
59
+ if (!list) {
60
+ list = [];
61
+ implementors.set(cls.extends, list);
62
+ }
63
+ if (!list.includes(cls.name)) list.push(cls.name);
64
+ }
65
+ }
66
+
67
+ // RTA: Phase 8.5 dedicated newExpressions list (all `new X()` in the file)
68
+ if (symbols.newExpressions) {
69
+ for (const typeName of symbols.newExpressions) {
70
+ instantiatedTypes.add(typeName);
71
+ }
72
+ }
73
+ // RTA fallback: constructor-confidence typeMap entries (confidence >= 0.9)
74
+ // covers codebases that haven't been re-parsed since Phase 8.5 was added.
75
+ if (symbols.typeMap instanceof Map) {
76
+ for (const entry of symbols.typeMap.values()) {
77
+ if (typeof entry !== 'string' && entry.confidence >= 0.9) {
78
+ instantiatedTypes.add(entry.type);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return { implementors, parents, instantiatedTypes };
85
+ }
86
+
87
+ // ── this / self / super resolution ──────────────────────────────────────────
88
+
89
+ /**
90
+ * Resolve `this.method()`, `self.method()`, or `super.method()` through the
91
+ * class hierarchy of the calling method.
92
+ *
93
+ * callerName must be a qualified method name ("ClassName.callerFn") for the
94
+ * class context to be determinable. Returns [] for plain functions.
95
+ *
96
+ * For `super`, resolution starts from the parent of the caller's class.
97
+ * For `this`/`self`, resolution starts from the caller's own class and walks
98
+ * up the inheritance chain (supporting inherited method lookup).
99
+ */
100
+ export function resolveThisDispatch(
101
+ methodName: string,
102
+ callerName: string | null,
103
+ receiver: 'this' | 'self' | 'super',
104
+ chaCtx: ChaContext,
105
+ lookup: CallNodeLookup,
106
+ ): ReadonlyArray<{ id: number; file: string }> {
107
+ if (!callerName) return [];
108
+ const dotIdx = callerName.indexOf('.');
109
+ if (dotIdx === -1) return [];
110
+
111
+ const callerClass = callerName.slice(0, dotIdx);
112
+ const startClass = receiver === 'super' ? chaCtx.parents.get(callerClass) : callerClass;
113
+ if (!startClass) return [];
114
+
115
+ // Walk up the hierarchy; the visited set guards against cycles in malformed data.
116
+ let current: string | undefined = startClass;
117
+ const visited = new Set<string>();
118
+ while (current && !visited.has(current)) {
119
+ visited.add(current);
120
+ const qualified = `${current}.${methodName}`;
121
+ const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
122
+ if (found.length > 0) return found;
123
+ current = chaCtx.parents.get(current);
124
+ }
125
+ return [];
126
+ }
127
+
128
+ // ── CHA dispatch expansion ───────────────────────────────────────────────────
129
+
130
+ /**
131
+ * CHA + RTA: given a receiver type (class or interface), return all concrete
132
+ * method implementations reachable via the class hierarchy.
133
+ *
134
+ * Only returns methods on types that are actually instantiated somewhere in
135
+ * the project (RTA filter). Returns [] when no concrete instantiated type
136
+ * overrides the given method.
137
+ *
138
+ * BFS over the implementors map handles multi-level hierarchies (e.g.
139
+ * IFoo → AbstractFoo → ConcreteFoo) so that abstract intermediate classes
140
+ * are transparently skipped while their concrete subclasses are still reached.
141
+ */
142
+ export function resolveChaTargets(
143
+ typeName: string,
144
+ methodName: string,
145
+ chaCtx: ChaContext,
146
+ lookup: CallNodeLookup,
147
+ ): ReadonlyArray<{ id: number; file: string }> {
148
+ const results: Array<{ id: number; file: string }> = [];
149
+
150
+ const queue: string[] = [typeName];
151
+ const visited = new Set<string>();
152
+ visited.add(typeName);
153
+
154
+ while (queue.length > 0) {
155
+ const current = queue.shift()!;
156
+ const children = chaCtx.implementors.get(current);
157
+ if (!children?.length) continue;
158
+
159
+ for (const cls of children) {
160
+ if (visited.has(cls)) continue;
161
+ visited.add(cls);
162
+
163
+ if (chaCtx.instantiatedTypes.has(cls)) {
164
+ const qualified = `${cls}.${methodName}`;
165
+ const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
166
+ results.push(...found);
167
+ }
168
+
169
+ // Traverse even non-instantiated classes — they may have instantiated subclasses.
170
+ queue.push(cls);
171
+ }
172
+ }
173
+
174
+ return results;
175
+ }
@@ -68,6 +68,8 @@ export class PipelineContext {
68
68
  batchResolved!: Map<string, string> | null;
69
69
  reexportMap!: Map<string, unknown[]>;
70
70
  barrelOnlyFiles!: Set<string>;
71
+ /** Phase 8.4: cache for resolveBarrelExport results keyed as "barrelPath|symbolName". */
72
+ barrelExportCache: Map<string, string | null> = new Map();
71
73
 
72
74
  // ── Node lookup (set by insertNodes / buildEdges stages) ───────────
73
75
  nodesByName!: Map<string, NodeRow[]>;
@@ -88,6 +90,7 @@ export class PipelineContext {
88
90
  edgeKind: string;
89
91
  confidence: number;
90
92
  dynamic: number;
93
+ technique: string | null;
91
94
  }> = [];
92
95
 
93
96
  // ── Misc state ─────────────────────────────────────────────────────
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { purgeFilesData } from '../../../db/index.js';
10
- import { warn } from '../../../infrastructure/logger.js';
10
+ import { debug, warn } from '../../../infrastructure/logger.js';
11
11
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
12
12
  import { compileGlobs, matchesAny } from '../../../shared/globs.js';
13
13
  import type {
@@ -313,9 +313,9 @@ function getEdgeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatem
313
313
  }
314
314
  let stmt = cache.get(chunkSize);
315
315
  if (!stmt) {
316
- const ph = '(?,?,?,?,?)';
316
+ const ph = '(?,?,?,?,?,?)';
317
317
  stmt = db.prepare(
318
- 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
318
+ 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic,technique) VALUES ' +
319
319
  Array.from({ length: chunkSize }, () => ph).join(','),
320
320
  );
321
321
  cache.set(chunkSize, stmt);
@@ -344,7 +344,7 @@ export function batchInsertNodes(db: BetterSqlite3Database, rows: unknown[][]):
344
344
 
345
345
  /**
346
346
  * Batch-insert edge rows via multi-value INSERT statements.
347
- * Each row: [source_id, target_id, kind, confidence, dynamic]
347
+ * Each row: [source_id, target_id, kind, confidence, dynamic, technique]
348
348
  */
349
349
  export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]): void {
350
350
  if (!rows.length) return;
@@ -355,8 +355,178 @@ export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]):
355
355
  const vals: unknown[] = [];
356
356
  for (let j = i; j < end; j++) {
357
357
  const r = rows[j] as unknown[];
358
- vals.push(r[0], r[1], r[2], r[3], r[4]);
358
+ vals.push(r[0], r[1], r[2], r[3], r[4], r[5] ?? null);
359
359
  }
360
360
  stmt.run(...vals);
361
361
  }
362
362
  }
363
+
364
+ /**
365
+ * CHA (Class Hierarchy Analysis) post-pass.
366
+ *
367
+ * Expands virtual-dispatch call edges for class hierarchies and interface
368
+ * implementations already present in the DB:
369
+ *
370
+ * 1. Build implementors map: parent/interface → [child/implementing class] from
371
+ * `extends` and `implements` edges.
372
+ * 2. Collect RTA evidence: class nodes that appear as `calls` targets (new X()).
373
+ * 3. Find all `calls` edges to qualified method nodes (name contains '.').
374
+ * 4. For each such call, expand to concrete overrides via the implementors map,
375
+ * filtered by RTA when evidence exists.
376
+ *
377
+ * Used by both the native orchestrator post-pass and the WASM build-edges pass.
378
+ */
379
+ export function runChaPostPass(db: BetterSqlite3Database): number {
380
+ const hasHierarchy = db
381
+ .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
382
+ .get();
383
+ if (!hasHierarchy) return 0;
384
+
385
+ const hierarchyRows = db
386
+ .prepare(
387
+ `SELECT src.name AS child_name, tgt.name AS parent_name
388
+ FROM edges e
389
+ JOIN nodes src ON e.source_id = src.id
390
+ JOIN nodes tgt ON e.target_id = tgt.id
391
+ WHERE e.kind IN ('extends', 'implements')`,
392
+ )
393
+ .all() as Array<{ child_name: string; parent_name: string }>;
394
+
395
+ const implementorSets = new Map<string, Set<string>>();
396
+ for (const row of hierarchyRows) {
397
+ let set = implementorSets.get(row.parent_name);
398
+ if (!set) {
399
+ set = new Set<string>();
400
+ implementorSets.set(row.parent_name, set);
401
+ }
402
+ set.add(row.child_name);
403
+ }
404
+ if (implementorSets.size === 0) return 0;
405
+ // Convert to arrays for iteration compatibility with the rest of the function
406
+ const implementors = new Map([...implementorSets.entries()].map(([k, v]) => [k, [...v]]));
407
+
408
+ // RTA: collect class names instantiated via constructor calls (`new X()`).
409
+ let rtaRows = db
410
+ .prepare(
411
+ `SELECT DISTINCT tgt.name
412
+ FROM edges e
413
+ JOIN nodes tgt ON e.target_id = tgt.id
414
+ WHERE e.kind = 'calls' AND tgt.kind = 'class'`,
415
+ )
416
+ .all() as Array<{ name: string }>;
417
+ if (rtaRows.length === 0) {
418
+ // Fallback: some languages (e.g. TypeScript via WASM) record constructor calls as
419
+ // 'function' or 'constructor' kind rather than 'class'. Restrict to names that are
420
+ // actually known class names to avoid treating unrelated function calls like `logger()`
421
+ // as class-instantiation evidence.
422
+ // Include both parent/interface names AND implementor (child) names so that
423
+ // `new UserRepository()` (a child class) is correctly detected as RTA evidence.
424
+ const knownClassNames = [
425
+ ...new Set([
426
+ ...implementorSets.keys(),
427
+ ...[...implementorSets.values()].flatMap((s) => [...s]),
428
+ ]),
429
+ ];
430
+ if (knownClassNames.length > 0) {
431
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
432
+ const CHUNK = 999;
433
+ for (let i = 0; i < knownClassNames.length; i += CHUNK) {
434
+ const chunk = knownClassNames.slice(i, i + CHUNK);
435
+ const placeholders = chunk.map(() => '?').join(',');
436
+ const chunkRows = db
437
+ .prepare(
438
+ `SELECT DISTINCT tgt.name
439
+ FROM edges e
440
+ JOIN nodes tgt ON e.target_id = tgt.id
441
+ WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
442
+ AND tgt.name IN (${placeholders})`,
443
+ )
444
+ .all(...chunk) as Array<{ name: string }>;
445
+ rtaRows = rtaRows.concat(chunkRows);
446
+ }
447
+ }
448
+ }
449
+ const instantiated = new Set(rtaRows.map((r) => r.name));
450
+ const noRtaEvidence = instantiated.size === 0;
451
+ if (noRtaEvidence) {
452
+ debug('runChaPostPass: no constructor-call evidence — proceeding without RTA filter');
453
+ }
454
+
455
+ const callToMethods = db
456
+ .prepare(
457
+ `SELECT e.source_id, tgt.name AS method_name
458
+ FROM edges e
459
+ JOIN nodes tgt ON e.target_id = tgt.id
460
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
461
+ AND INSTR(tgt.name, '.') > 0`,
462
+ )
463
+ .all() as Array<{ source_id: number; method_name: string }>;
464
+
465
+ const seen = new Set<string>();
466
+ // Scope deduplication to only the source_ids we are about to expand, avoiding
467
+ // a full-table scan. CHA only inserts edges FROM callers that already call a
468
+ // qualified method (the source_ids in callToMethods), so we only need to
469
+ // check existing edges for those specific callers.
470
+ const callerIds = [...new Set(callToMethods.map((r) => r.source_id))];
471
+ if (callerIds.length > 0) {
472
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
473
+ const CHUNK = 999;
474
+ for (let i = 0; i < callerIds.length; i += CHUNK) {
475
+ const chunk = callerIds.slice(i, i + CHUNK);
476
+ const placeholders = chunk.map(() => '?').join(',');
477
+ const existingPairs = db
478
+ .prepare(
479
+ `SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`,
480
+ )
481
+ .all(...chunk) as Array<{ source_id: number; target_id: number }>;
482
+ for (const e of existingPairs) seen.add(`${e.source_id}|${e.target_id}`);
483
+ }
484
+ }
485
+
486
+ // No LIMIT: multiple files can define the same qualified name in a monorepo.
487
+ const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
488
+ const newEdges: Array<[number, number, string, number, number, string]> = [];
489
+
490
+ for (const { source_id, method_name } of callToMethods) {
491
+ const dotIdx = method_name.indexOf('.');
492
+ if (dotIdx === -1) continue;
493
+ const typeName = method_name.slice(0, dotIdx);
494
+ const methodSuffix = method_name.slice(dotIdx + 1);
495
+
496
+ // BFS over the implementors map — handles multi-level hierarchies where
497
+ // abstract/non-instantiated classes sit between the call-site type and
498
+ // the concrete leaf implementations (matches runPostNativeCha, issue #1311).
499
+ const bfsQueue: string[] = [typeName];
500
+ const bfsVisited = new Set<string>([typeName]);
501
+ while (bfsQueue.length > 0) {
502
+ const current = bfsQueue.shift()!;
503
+ const children = implementors.get(current);
504
+ if (!children?.length) continue;
505
+
506
+ for (const cls of children) {
507
+ if (bfsVisited.has(cls)) continue;
508
+ bfsVisited.add(cls);
509
+
510
+ if (noRtaEvidence || instantiated.has(cls)) {
511
+ const qualifiedName = `${cls}.${methodSuffix}`;
512
+ const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
513
+ for (const methodNode of methodNodes) {
514
+ const key = `${source_id}|${methodNode.id}`;
515
+ if (seen.has(key)) continue;
516
+ seen.add(key);
517
+ newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
518
+ }
519
+ }
520
+
521
+ // Always traverse children — non-instantiated classes may have instantiated subclasses.
522
+ bfsQueue.push(cls);
523
+ }
524
+ }
525
+ }
526
+
527
+ if (newEdges.length > 0) {
528
+ db.transaction(() => batchInsertEdges(db, newEdges))();
529
+ debug(`runChaPostPass: inserted ${newEdges.length} CHA dispatch edge(s)`);
530
+ }
531
+ return newEdges.length;
532
+ }