@optave/codegraph 3.11.2 → 3.13.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 (236) hide show
  1. package/README.md +73 -37
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +2 -1
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/batch.d.ts.map +1 -1
  6. package/dist/cli/commands/batch.js +1 -0
  7. package/dist/cli/commands/batch.js.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/build.js +6 -1
  10. package/dist/cli/commands/build.js.map +1 -1
  11. package/dist/cli/commands/config.d.ts +3 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +272 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/triage.js +1 -1
  16. package/dist/cli/commands/triage.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +10 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/shared/options.d.ts +2 -1
  21. package/dist/cli/shared/options.d.ts.map +1 -1
  22. package/dist/cli/shared/options.js +11 -1
  23. package/dist/cli/shared/options.js.map +1 -1
  24. package/dist/cli/types.d.ts +2 -0
  25. package/dist/cli/types.d.ts.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/migrations.js +8 -1
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/domain/analysis/module-map.d.ts +2 -0
  30. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  31. package/dist/domain/analysis/module-map.js +24 -2
  32. package/dist/domain/analysis/module-map.js.map +1 -1
  33. package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
  34. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  35. package/dist/domain/graph/builder/call-resolver.js +251 -34
  36. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  37. package/dist/domain/graph/builder/cha.d.ts +69 -0
  38. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  39. package/dist/domain/graph/builder/cha.js +158 -0
  40. package/dist/domain/graph/builder/cha.js.map +1 -0
  41. package/dist/domain/graph/builder/context.d.ts +3 -0
  42. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/context.js +2 -0
  44. package/dist/domain/graph/builder/context.js.map +1 -1
  45. package/dist/domain/graph/builder/helpers.d.ts +25 -1
  46. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/helpers.js +178 -5
  48. package/dist/domain/graph/builder/helpers.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +74 -2
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/pipeline.js +37 -2
  54. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-edges.js +704 -34
  57. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  59. package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
  60. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  63. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  68. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  71. package/dist/domain/graph/journal.js +1 -1
  72. package/dist/domain/graph/journal.js.map +1 -1
  73. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  74. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  75. package/dist/domain/graph/resolver/points-to.js +213 -0
  76. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  77. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  78. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  79. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  80. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  81. package/dist/domain/parser.d.ts +12 -4
  82. package/dist/domain/parser.d.ts.map +1 -1
  83. package/dist/domain/parser.js +83 -20
  84. package/dist/domain/parser.js.map +1 -1
  85. package/dist/domain/wasm-worker-entry.js +35 -2
  86. package/dist/domain/wasm-worker-entry.js.map +1 -1
  87. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  88. package/dist/domain/wasm-worker-pool.js +34 -0
  89. package/dist/domain/wasm-worker-pool.js.map +1 -1
  90. package/dist/domain/wasm-worker-protocol.d.ts +15 -1
  91. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  92. package/dist/extractors/c.js +3 -3
  93. package/dist/extractors/c.js.map +1 -1
  94. package/dist/extractors/clojure.js +1 -1
  95. package/dist/extractors/clojure.js.map +1 -1
  96. package/dist/extractors/cpp.d.ts.map +1 -1
  97. package/dist/extractors/cpp.js +45 -4
  98. package/dist/extractors/cpp.js.map +1 -1
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +37 -8
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/cuda.d.ts.map +1 -1
  103. package/dist/extractors/cuda.js +45 -4
  104. package/dist/extractors/cuda.js.map +1 -1
  105. package/dist/extractors/elixir.js +6 -6
  106. package/dist/extractors/elixir.js.map +1 -1
  107. package/dist/extractors/fsharp.js +1 -1
  108. package/dist/extractors/fsharp.js.map +1 -1
  109. package/dist/extractors/go.js +5 -5
  110. package/dist/extractors/go.js.map +1 -1
  111. package/dist/extractors/haskell.js +1 -1
  112. package/dist/extractors/haskell.js.map +1 -1
  113. package/dist/extractors/helpers.d.ts +11 -0
  114. package/dist/extractors/helpers.d.ts.map +1 -1
  115. package/dist/extractors/helpers.js +40 -0
  116. package/dist/extractors/helpers.js.map +1 -1
  117. package/dist/extractors/java.d.ts.map +1 -1
  118. package/dist/extractors/java.js +10 -9
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.d.ts +2 -0
  121. package/dist/extractors/javascript.d.ts.map +1 -1
  122. package/dist/extractors/javascript.js +1812 -71
  123. package/dist/extractors/javascript.js.map +1 -1
  124. package/dist/extractors/kotlin.js +5 -5
  125. package/dist/extractors/kotlin.js.map +1 -1
  126. package/dist/extractors/lua.js +1 -1
  127. package/dist/extractors/lua.js.map +1 -1
  128. package/dist/extractors/objc.js +3 -3
  129. package/dist/extractors/objc.js.map +1 -1
  130. package/dist/extractors/ocaml.js +1 -1
  131. package/dist/extractors/ocaml.js.map +1 -1
  132. package/dist/extractors/php.js +2 -2
  133. package/dist/extractors/php.js.map +1 -1
  134. package/dist/extractors/python.js +7 -7
  135. package/dist/extractors/python.js.map +1 -1
  136. package/dist/extractors/ruby.js +2 -2
  137. package/dist/extractors/ruby.js.map +1 -1
  138. package/dist/extractors/scala.js +1 -1
  139. package/dist/extractors/scala.js.map +1 -1
  140. package/dist/extractors/solidity.js +1 -1
  141. package/dist/extractors/solidity.js.map +1 -1
  142. package/dist/extractors/swift.js +4 -4
  143. package/dist/extractors/swift.js.map +1 -1
  144. package/dist/extractors/zig.js +4 -4
  145. package/dist/extractors/zig.js.map +1 -1
  146. package/dist/features/structure-query.d.ts +1 -1
  147. package/dist/features/structure-query.d.ts.map +1 -1
  148. package/dist/features/structure-query.js +6 -6
  149. package/dist/features/structure-query.js.map +1 -1
  150. package/dist/index.d.ts +1 -1
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +1 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/infrastructure/config.d.ts +85 -2
  155. package/dist/infrastructure/config.d.ts.map +1 -1
  156. package/dist/infrastructure/config.js +408 -19
  157. package/dist/infrastructure/config.js.map +1 -1
  158. package/dist/infrastructure/native.d.ts +11 -0
  159. package/dist/infrastructure/native.d.ts.map +1 -1
  160. package/dist/infrastructure/native.js +78 -5
  161. package/dist/infrastructure/native.js.map +1 -1
  162. package/dist/infrastructure/registry.d.ts +27 -0
  163. package/dist/infrastructure/registry.d.ts.map +1 -1
  164. package/dist/infrastructure/registry.js +59 -1
  165. package/dist/infrastructure/registry.js.map +1 -1
  166. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  167. package/dist/presentation/queries-cli/overview.js +5 -0
  168. package/dist/presentation/queries-cli/overview.js.map +1 -1
  169. package/dist/presentation/structure.d.ts +1 -1
  170. package/dist/presentation/structure.d.ts.map +1 -1
  171. package/dist/presentation/structure.js +2 -2
  172. package/dist/presentation/structure.js.map +1 -1
  173. package/dist/types.d.ts +221 -0
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-gleam.wasm +0 -0
  176. package/package.json +7 -8
  177. package/src/cli/commands/audit.ts +2 -1
  178. package/src/cli/commands/batch.ts +1 -0
  179. package/src/cli/commands/build.ts +6 -1
  180. package/src/cli/commands/config.ts +353 -0
  181. package/src/cli/commands/triage.ts +1 -1
  182. package/src/cli/index.ts +10 -0
  183. package/src/cli/shared/options.ts +11 -1
  184. package/src/cli/types.ts +2 -0
  185. package/src/db/migrations.ts +8 -1
  186. package/src/domain/analysis/module-map.ts +29 -1
  187. package/src/domain/graph/builder/call-resolver.ts +263 -35
  188. package/src/domain/graph/builder/cha.ts +192 -0
  189. package/src/domain/graph/builder/context.ts +3 -0
  190. package/src/domain/graph/builder/helpers.ts +195 -5
  191. package/src/domain/graph/builder/incremental.ts +80 -1
  192. package/src/domain/graph/builder/pipeline.ts +49 -2
  193. package/src/domain/graph/builder/stages/build-edges.ts +867 -32
  194. package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
  195. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  196. package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
  197. package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
  198. package/src/domain/graph/journal.ts +1 -1
  199. package/src/domain/graph/resolver/points-to.ts +254 -0
  200. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  201. package/src/domain/parser.ts +86 -17
  202. package/src/domain/wasm-worker-entry.ts +35 -2
  203. package/src/domain/wasm-worker-pool.ts +22 -0
  204. package/src/domain/wasm-worker-protocol.ts +15 -0
  205. package/src/extractors/c.ts +3 -3
  206. package/src/extractors/clojure.ts +1 -1
  207. package/src/extractors/cpp.ts +47 -4
  208. package/src/extractors/csharp.ts +33 -9
  209. package/src/extractors/cuda.ts +47 -4
  210. package/src/extractors/elixir.ts +6 -6
  211. package/src/extractors/fsharp.ts +1 -1
  212. package/src/extractors/go.ts +5 -5
  213. package/src/extractors/haskell.ts +1 -1
  214. package/src/extractors/helpers.ts +43 -0
  215. package/src/extractors/java.ts +10 -9
  216. package/src/extractors/javascript.ts +1929 -72
  217. package/src/extractors/kotlin.ts +5 -5
  218. package/src/extractors/lua.ts +1 -1
  219. package/src/extractors/objc.ts +3 -3
  220. package/src/extractors/ocaml.ts +1 -1
  221. package/src/extractors/php.ts +2 -2
  222. package/src/extractors/python.ts +7 -7
  223. package/src/extractors/ruby.ts +2 -2
  224. package/src/extractors/scala.ts +1 -1
  225. package/src/extractors/solidity.ts +1 -1
  226. package/src/extractors/swift.ts +4 -4
  227. package/src/extractors/zig.ts +4 -4
  228. package/src/features/structure-query.ts +7 -7
  229. package/src/index.ts +5 -1
  230. package/src/infrastructure/config.ts +494 -20
  231. package/src/infrastructure/native.ts +87 -5
  232. package/src/infrastructure/registry.ts +82 -1
  233. package/src/presentation/queries-cli/overview.ts +15 -1
  234. package/src/presentation/structure.ts +3 -3
  235. package/src/types.ts +235 -0
  236. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -23,8 +23,46 @@ 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
 
50
+ /**
51
+ * Callable definition kinds — variable/constant bindings are NOT callable
52
+ * in the function-as-enclosing-scope sense (they are local declarations, not
53
+ * function bodies). Top-level variable bindings (e.g. Haskell `main = do …`)
54
+ * are handled separately as a fallback tier.
55
+ */
56
+ const CALLABLE_KINDS = new Set(['function', 'method']);
57
+
58
+ /**
59
+ * Variable-like binding kinds that may act as top-level callers when no
60
+ * enclosing function/method exists (e.g. Haskell top-level `main` is a
61
+ * `bind` node → kind `variable`). Local variable declarations inside a
62
+ * function body must NOT win over the enclosing function.
63
+ */
64
+ const TOP_LEVEL_BINDING_KINDS = new Set(['variable', 'constant']);
65
+
28
66
  export function findCaller(
29
67
  lookup: CallNodeLookup,
30
68
  call: { line: number },
@@ -36,25 +74,64 @@ export function findCaller(
36
74
  }>,
37
75
  relPath: string,
38
76
  fileNodeRow: { id: number },
39
- ): { id: number } {
40
- let caller: { id: number } | null = null;
41
- let callerSpan = Infinity;
77
+ ): { id: number; callerName: string | null } {
78
+ // Pass 1: find the narrowest enclosing function/method.
79
+ let fnCaller: { id: number } | null = null;
80
+ let fnCallerName: string | null = null;
81
+ let fnCallerSpan = Infinity;
82
+
83
+ // Pass 2: find the widest (outermost) enclosing variable/constant binding.
84
+ // Used as fallback when no function/method encloses the call site
85
+ // (e.g. Haskell `main = do …` is a `bind` node with kind `variable`).
86
+ // We pick the WIDEST span (outermost binding), not the narrowest, so that
87
+ // nested `let` bindings inside `main`'s do-block do not shadow `main`
88
+ // itself as the attributing caller. The outermost enclosing variable is
89
+ // the "function-like" top-level binding.
90
+ let varCaller: { id: number } | null = null;
91
+ let varCallerName: string | null = null;
92
+ let varCallerSpan = -1; // looking for WIDEST span, so start at -1
93
+
42
94
  for (const def of definitions) {
43
95
  if (def.line <= call.line) {
44
- const end = def.endLine || Infinity;
96
+ const end = def.endLine ?? Infinity;
45
97
  if (call.line <= end) {
46
- const span = end - def.line;
47
- if (span < callerSpan) {
48
- const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
49
- if (row) {
50
- caller = row;
51
- callerSpan = span;
98
+ const span = end === Infinity ? Infinity : end - def.line;
99
+ if (CALLABLE_KINDS.has(def.kind)) {
100
+ if (span < fnCallerSpan) {
101
+ const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
102
+ if (row) {
103
+ fnCaller = row;
104
+ fnCallerName = def.name;
105
+ fnCallerSpan = span;
106
+ }
107
+ }
108
+ } else if (TOP_LEVEL_BINDING_KINDS.has(def.kind)) {
109
+ if (span > varCallerSpan) {
110
+ const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
111
+ if (row) {
112
+ varCaller = row;
113
+ varCallerName = def.name;
114
+ varCallerSpan = span;
115
+ }
52
116
  }
53
117
  }
54
118
  }
55
119
  }
56
120
  }
57
- return caller ?? fileNodeRow;
121
+
122
+ // Prefer function/method enclosing scope over variable binding.
123
+ // If a function/method encloses the call, use it — local variable
124
+ // declarations inside the function body must not shadow it.
125
+ // Only fall back to a variable/constant binding when the call is at
126
+ // top-level scope (no enclosing function/method found), which handles
127
+ // languages like Haskell where `main` is a top-level `bind` node.
128
+ if (fnCaller) {
129
+ return { ...fnCaller, callerName: fnCallerName };
130
+ }
131
+ if (varCaller) {
132
+ return { ...varCaller, callerName: varCallerName };
133
+ }
134
+ return { ...fileNodeRow, callerName: null };
58
135
  }
59
136
 
60
137
  export function resolveByMethodOrGlobal(
@@ -62,17 +139,104 @@ export function resolveByMethodOrGlobal(
62
139
  call: { name: string; receiver?: string | null },
63
140
  relPath: string,
64
141
  typeMap: Map<string, unknown>,
142
+ callerName?: string | null,
65
143
  ): ReadonlyArray<{ id: number; file: string }> {
66
144
  if (call.receiver) {
67
- const typeEntry = typeMap.get(call.receiver);
68
- const typeName = typeEntry
145
+ // Strip "this." so `this.repo.method()` resolves via typeMap["repo"]
146
+ // (or the "this.repo" key seeded directly by the TSC property-declaration enricher).
147
+ const effectiveReceiver = call.receiver.startsWith('this.')
148
+ ? call.receiver.slice('this.'.length)
149
+ : call.receiver;
150
+ // For this.prop receivers, prefer the class-scoped key (ClassName.prop) seeded by
151
+ // handlePropWriteTypeMap / handleFieldDefTypeMap — prevents false edges when multiple
152
+ // classes define the same property name (issues #1323, #1458).
153
+ // Class-scoped lookup runs first so bare fallback keys (confidence 0.6) don't shadow
154
+ // the correct per-class entry when callerName is available.
155
+ let typeEntry: unknown;
156
+ if (call.receiver.startsWith('this.') && callerName) {
157
+ const dotIdx = callerName.lastIndexOf('.');
158
+ if (dotIdx > -1) {
159
+ const callerClass = callerName.slice(0, dotIdx);
160
+ typeEntry = typeMap.get(`${callerClass}.${effectiveReceiver}`);
161
+ }
162
+ }
163
+ typeEntry ??=
164
+ typeMap.get(effectiveReceiver) ??
165
+ typeMap.get(call.receiver) ??
166
+ // Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
167
+ // same-name rest-binding collision across functions in the same file (#1358).
168
+ (callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
169
+ let typeName = typeEntry
69
170
  ? typeof typeEntry === 'string'
70
171
  ? typeEntry
71
172
  : (typeEntry as { type?: string }).type
72
173
  : null;
174
+
175
+ // Belt-and-suspenders fallback for inline new-expression receivers that
176
+ // extractReceiverName did not normalise (e.g. raw text leaked from an
177
+ // unhandled AST node type). extractReceiverName already handles the common
178
+ // `new_expression` / `parenthesized_expression(new_expression)` shapes by
179
+ // returning the constructor name directly, so this branch is exercised only
180
+ // by future node types or constructs that fall through to the raw-text path.
181
+ // The uppercase-initial restriction ([A-Z_$]) is a heuristic to distinguish
182
+ // constructors (PascalCase) from regular functions and avoids false positives
183
+ // on `(new xmlParser()).parse()` style calls.
184
+ if (!typeName && call.receiver) {
185
+ const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver);
186
+ if (m?.[1]) typeName = m[1];
187
+ }
188
+
73
189
  if (typeName) {
74
- const typed = lookup.byName(`${typeName}.${call.name}`).filter((n) => n.kind === 'method');
190
+ const typed = lookup
191
+ .byName(`${typeName}.${call.name}`)
192
+ .filter((n) => n.kind === 'method' && computeConfidence(relPath, n.file, null) >= 0.5);
75
193
  if (typed.length > 0) return typed;
194
+
195
+ // Prototype alias: `Foo.prototype.bar = identifier` seeds typeMap['Foo.bar'] = { type: identifier }.
196
+ // Checked after the symbol-DB lookup so an actual method definition always wins.
197
+ const protoEntry = typeMap.get(`${typeName}.${call.name}`);
198
+ const protoTarget = protoEntry
199
+ ? typeof protoEntry === 'string'
200
+ ? protoEntry
201
+ : (protoEntry as { type?: string }).type
202
+ : null;
203
+ if (protoTarget) {
204
+ const resolved = lookup
205
+ .byName(protoTarget)
206
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
207
+ if (resolved.length > 0) return resolved;
208
+ }
209
+ }
210
+
211
+ // Direct qualified method lookup: ClassName.staticMethod() or ClassName.instanceMethod()
212
+ // when the receiver is a class name with no typeMap entry. Handles static method calls
213
+ // like `C6.staticMethod()` or `D.d()` where the receiver IS the class.
214
+ // Matches both 'method' and 'function' kinds to cover field-initializer synthetic defs.
215
+ if (!typeName) {
216
+ const qualifiedName = `${effectiveReceiver}.${call.name}`;
217
+ const direct = lookup
218
+ .byName(qualifiedName)
219
+ .filter(
220
+ (n) =>
221
+ (n.kind === 'method' || n.kind === 'function') &&
222
+ computeConfidence(relPath, n.file, null) >= 0.5,
223
+ );
224
+ if (direct.length > 0) return direct;
225
+ }
226
+
227
+ // Phase 8.3d: composite pts key — `obj.prop = fn` seeds typeMap['obj.prop'] = { type: 'fn' }.
228
+ // When a call site references `obj.prop` as a callback, resolve directly to the target fn.
229
+ const compositeEntry = typeMap.get(`${call.receiver}.${call.name}`);
230
+ const ptsTarget = compositeEntry
231
+ ? typeof compositeEntry === 'string'
232
+ ? compositeEntry
233
+ : (compositeEntry as { type?: string }).type
234
+ : null;
235
+ if (ptsTarget) {
236
+ const resolved = lookup
237
+ .byName(ptsTarget)
238
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
239
+ if (resolved.length > 0) return resolved;
76
240
  }
77
241
  }
78
242
  if (
@@ -81,7 +245,68 @@ export function resolveByMethodOrGlobal(
81
245
  call.receiver === 'self' ||
82
246
  call.receiver === 'super'
83
247
  ) {
84
- return lookup.byName(call.name).filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
248
+ // Phase 8.3f: accessor this-dispatch via Object.defineProperty.
249
+ // When a plain function (no class prefix) is registered as a get/set accessor for `obj`
250
+ // via Object.defineProperty, typeMap seeds 'callerName:this' = 'obj'.
251
+ // We then resolve this.method() → typeMap['obj.method'] → the concrete definition.
252
+ // This runs before the broad exact-name lookup to avoid false positives from
253
+ // unrelated same-file definitions.
254
+ if (call.receiver === 'this' && callerName && !callerName.includes('.')) {
255
+ const accessorThisEntry = typeMap.get(`${callerName}:this`);
256
+ const objName = accessorThisEntry
257
+ ? typeof accessorThisEntry === 'string'
258
+ ? accessorThisEntry
259
+ : (accessorThisEntry as { type?: string }).type
260
+ : null;
261
+ if (objName) {
262
+ const objMethodEntry = typeMap.get(`${objName}.${call.name}`);
263
+ const targetFn = objMethodEntry
264
+ ? typeof objMethodEntry === 'string'
265
+ ? objMethodEntry
266
+ : (objMethodEntry as { type?: string }).type
267
+ : null;
268
+ if (targetFn) {
269
+ const resolved = lookup
270
+ .byName(targetFn)
271
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
272
+ if (resolved.length > 0) return resolved;
273
+ }
274
+ }
275
+ }
276
+
277
+ const exact = lookup
278
+ .byName(call.name)
279
+ .filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
280
+ if (exact.length > 0) return exact;
281
+
282
+ // Try same-class method lookup via callerName.
283
+ // e.g. `this.area()` inside `Shape.describe` → try `Shape.area`.
284
+ // Also covers no-receiver calls inside class methods, e.g. `IsValidEmail(x)` inside
285
+ // `Validators.ValidateUser` → try `Validators.IsValidEmail` (C#/Java static siblings).
286
+ // This seeds the initial edge that runChaPostPass later expands to subclass overrides.
287
+ //
288
+ // For JS/TS, bare (no-receiver) calls are module-scoped — there is no implicit class
289
+ // binding. Skip the same-class fallback for bare calls in those languages to prevent
290
+ // false positives (e.g. `flush()` inside `Processor.run` must not resolve to
291
+ // `Processor.flush`). this.method() calls are unaffected: they still reach the fallback
292
+ // because `call.receiver === 'this'` is truthy, not a bare call.
293
+ const isBareCall = !call.receiver;
294
+ if (callerName && !(isBareCall && isModuleScopedLanguage(relPath))) {
295
+ const dotIdx = callerName.lastIndexOf('.');
296
+ if (dotIdx > -1) {
297
+ // Extract only the segment immediately before the method name so that
298
+ // 'Namespace.ClassName.method' yields 'ClassName', not 'Namespace.ClassName'.
299
+ // Symbols are stored under their bare class name, not their qualified path.
300
+ const prevDot = callerName.lastIndexOf('.', dotIdx - 1);
301
+ const callerClass = callerName.slice(prevDot + 1, dotIdx);
302
+ const qualifiedName = `${callerClass}.${call.name}`;
303
+ const sameClass = lookup
304
+ .byName(qualifiedName)
305
+ .filter((t) => t.kind === 'method' && computeConfidence(relPath, t.file, null) >= 0.5);
306
+ if (sameClass.length > 0) return sameClass;
307
+ }
308
+ }
309
+ return exact; // empty
85
310
  }
86
311
  return [];
87
312
  }
@@ -92,6 +317,7 @@ export function resolveCallTargets(
92
317
  relPath: string,
93
318
  importedNames: Map<string, string>,
94
319
  typeMap: Map<string, unknown>,
320
+ callerName?: string | null,
95
321
  ): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
96
322
  const importedFrom = importedNames.get(call.name);
97
323
  let targets: ReadonlyArray<{ id: number; file: string }> | undefined;
@@ -109,7 +335,7 @@ export function resolveCallTargets(
109
335
  if (!targets || targets.length === 0) {
110
336
  targets = lookup.byNameAndFile(call.name, relPath);
111
337
  if (targets.length === 0) {
112
- targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap);
338
+ targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap, callerName);
113
339
  }
114
340
  }
115
341
 
@@ -129,13 +355,17 @@ export function resolveCallTargets(
129
355
  * Returns the edge tuple to insert, or null if nothing matched or the edge
130
356
  * was already seen. Callers are responsible for the actual DB/array insert.
131
357
  *
132
- * Receiver resolution collects all same-file candidates first (no kind
133
- * filter), falls back to global candidates only when the same-file set is
134
- * entirely empty, then filters the chosen set by RECEIVER_KINDS. This
135
- * matches the native Rust build path: if a file imports a name that happens
136
- * to be emitted as `kind='function'` in the importer, the same-file set is
137
- * non-empty and blocks the global fallback, so no receiver edge is emitted.
138
- * Keeping this behaviour identical to the Rust path maintains engine parity.
358
+ * Receiver resolution:
359
+ * 1. Look up same-file nodes for `effectiveReceiver` (unfiltered by kind).
360
+ * 2. If any same-file node exists AND `effectiveReceiver` is not in `importedNames`
361
+ * (i.e. it is a locally-defined symbol, not an import artifact), apply
362
+ * RECEIVER_KINDS and return the filtered set no global fallback.
363
+ * A local `function C(){}` means this file owns `C`; no cross-file class
364
+ * should win over it (issue #1539).
365
+ * 3. If the same-file node IS an import artifact (e.g. destructured require),
366
+ * or no same-file node exists at all, fall back to global candidates filtered
367
+ * by RECEIVER_KINDS. This preserves the pre-#1539 behaviour for cases where
368
+ * an imported name appears as kind='function' in the importer file.
139
369
  */
140
370
  export function resolveReceiverEdge(
141
371
  lookup: CallNodeLookup,
@@ -144,6 +374,7 @@ export function resolveReceiverEdge(
144
374
  relPath: string,
145
375
  typeMap: Map<string, unknown>,
146
376
  seenCallEdges: Set<string>,
377
+ importedNames: ReadonlyMap<string, string>,
147
378
  ): { callerId: number; receiverId: number; confidence: number } | null {
148
379
  const typeEntry = typeMap.get(call.receiver);
149
380
  const typeName = typeEntry
@@ -156,18 +387,15 @@ export function resolveReceiverEdge(
156
387
  ? ((typeEntry as { confidence?: number }).confidence ?? null)
157
388
  : null;
158
389
  const effectiveReceiver = typeName || call.receiver;
159
- // Filter-before: apply RECEIVER_KINDS to same-file candidates first, then
160
- // fall back to global candidates (also filtered) only when same-file yields
161
- // nothing. This prevents an imported name emitted as kind='function' in the
162
- // importing file from blocking the fallback to the actual class/struct/etc.
163
- // node in the defining file.
164
- const sameFileCandidates = lookup
165
- .byNameAndFile(effectiveReceiver, relPath)
166
- .filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
167
- const candidates =
168
- sameFileCandidates.length > 0
169
- ? sameFileCandidates
170
- : lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
390
+ // Block global fallback only when the same-file node is a local definition,
391
+ // not when it's an import artifact (e.g. `const { C } = require(…)` seeds a
392
+ // kind='function' node in the importer but the real class lives elsewhere).
393
+ const sameFileAll = lookup.byNameAndFile(effectiveReceiver, relPath);
394
+ const isLocalDefinition = sameFileAll.length > 0 && !importedNames?.has(effectiveReceiver);
395
+ const sameFileCandidates = sameFileAll.filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
396
+ const candidates = isLocalDefinition
397
+ ? sameFileCandidates
398
+ : lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
171
399
  if (candidates.length === 0) return null;
172
400
  const recvTarget = candidates[0]!;
173
401
  const recvKey = `recv|${caller.id}|${recvTarget.id}`;
@@ -0,0 +1,192 @@
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
+ * When `callerFile` is provided, same-file method nodes are preferred: if the
101
+ * hierarchy walk finds a qualified method that exists in both the caller's own
102
+ * file AND in unrelated files (e.g. a class named `A` that appears in multiple
103
+ * fixture files), only the same-file nodes are returned. This prevents
104
+ * cross-fixture false edges caused by accidental name collisions across
105
+ * unrelated files in the same project build. When no same-file nodes exist,
106
+ * all found nodes are returned as before.
107
+ */
108
+ export function resolveThisDispatch(
109
+ methodName: string,
110
+ callerName: string | null,
111
+ receiver: 'this' | 'self' | 'super',
112
+ chaCtx: ChaContext,
113
+ lookup: CallNodeLookup,
114
+ callerFile?: string | null,
115
+ ): ReadonlyArray<{ id: number; file: string }> {
116
+ if (!callerName) return [];
117
+ const dotIdx = callerName.indexOf('.');
118
+ if (dotIdx === -1) return [];
119
+
120
+ const callerClass = callerName.slice(0, dotIdx);
121
+ const startClass = receiver === 'super' ? chaCtx.parents.get(callerClass) : callerClass;
122
+ if (!startClass) return [];
123
+
124
+ // Walk up the hierarchy; the visited set guards against cycles in malformed data.
125
+ let current: string | undefined = startClass;
126
+ const visited = new Set<string>();
127
+ while (current && !visited.has(current)) {
128
+ visited.add(current);
129
+ const qualified = `${current}.${methodName}`;
130
+ const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
131
+ if (found.length > 0) {
132
+ // When the caller's file is known, prefer same-file nodes to avoid
133
+ // emitting cross-file edges to identically-named methods in unrelated
134
+ // files. Only fall back to the full set when no same-file node exists.
135
+ if (callerFile && found.some((n) => n.file === callerFile)) {
136
+ return found.filter((n) => n.file === callerFile);
137
+ }
138
+ return found;
139
+ }
140
+ current = chaCtx.parents.get(current);
141
+ }
142
+ return [];
143
+ }
144
+
145
+ // ── CHA dispatch expansion ───────────────────────────────────────────────────
146
+
147
+ /**
148
+ * CHA + RTA: given a receiver type (class or interface), return all concrete
149
+ * method implementations reachable via the class hierarchy.
150
+ *
151
+ * Only returns methods on types that are actually instantiated somewhere in
152
+ * the project (RTA filter). Returns [] when no concrete instantiated type
153
+ * overrides the given method.
154
+ *
155
+ * BFS over the implementors map handles multi-level hierarchies (e.g.
156
+ * IFoo → AbstractFoo → ConcreteFoo) so that abstract intermediate classes
157
+ * are transparently skipped while their concrete subclasses are still reached.
158
+ */
159
+ export function resolveChaTargets(
160
+ typeName: string,
161
+ methodName: string,
162
+ chaCtx: ChaContext,
163
+ lookup: CallNodeLookup,
164
+ ): ReadonlyArray<{ id: number; file: string }> {
165
+ const results: Array<{ id: number; file: string }> = [];
166
+
167
+ const queue: string[] = [typeName];
168
+ const visited = new Set<string>();
169
+ visited.add(typeName);
170
+
171
+ while (queue.length > 0) {
172
+ const current = queue.shift()!;
173
+ const children = chaCtx.implementors.get(current);
174
+ if (!children?.length) continue;
175
+
176
+ for (const cls of children) {
177
+ if (visited.has(cls)) continue;
178
+ visited.add(cls);
179
+
180
+ if (chaCtx.instantiatedTypes.has(cls)) {
181
+ const qualified = `${cls}.${methodName}`;
182
+ const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
183
+ results.push(...found);
184
+ }
185
+
186
+ // Traverse even non-instantiated classes — they may have instantiated subclasses.
187
+ queue.push(cls);
188
+ }
189
+ }
190
+
191
+ return results;
192
+ }
@@ -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 ─────────────────────────────────────────────────────