@optave/codegraph 3.11.1 → 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 (176) 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 +73 -0
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  11. package/dist/domain/graph/builder/call-resolver.js +292 -0
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  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 +147 -54
  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 +932 -110
  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/graph/watcher.d.ts.map +1 -1
  53. package/dist/domain/graph/watcher.js +5 -2
  54. package/dist/domain/graph/watcher.js.map +1 -1
  55. package/dist/domain/parser.d.ts +10 -1
  56. package/dist/domain/parser.d.ts.map +1 -1
  57. package/dist/domain/parser.js +39 -7
  58. package/dist/domain/parser.js.map +1 -1
  59. package/dist/domain/wasm-worker-entry.js +25 -0
  60. package/dist/domain/wasm-worker-entry.js.map +1 -1
  61. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  62. package/dist/domain/wasm-worker-pool.js +32 -0
  63. package/dist/domain/wasm-worker-pool.js.map +1 -1
  64. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  65. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  66. package/dist/extractors/c.js +3 -3
  67. package/dist/extractors/c.js.map +1 -1
  68. package/dist/extractors/clojure.js +1 -1
  69. package/dist/extractors/clojure.js.map +1 -1
  70. package/dist/extractors/cpp.js +3 -3
  71. package/dist/extractors/cpp.js.map +1 -1
  72. package/dist/extractors/csharp.d.ts.map +1 -1
  73. package/dist/extractors/csharp.js +37 -8
  74. package/dist/extractors/csharp.js.map +1 -1
  75. package/dist/extractors/cuda.js +3 -3
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/elixir.js +6 -6
  78. package/dist/extractors/elixir.js.map +1 -1
  79. package/dist/extractors/fsharp.js +1 -1
  80. package/dist/extractors/fsharp.js.map +1 -1
  81. package/dist/extractors/go.js +5 -5
  82. package/dist/extractors/go.js.map +1 -1
  83. package/dist/extractors/haskell.js +1 -1
  84. package/dist/extractors/haskell.js.map +1 -1
  85. package/dist/extractors/java.js +2 -2
  86. package/dist/extractors/java.js.map +1 -1
  87. package/dist/extractors/javascript.d.ts +2 -0
  88. package/dist/extractors/javascript.d.ts.map +1 -1
  89. package/dist/extractors/javascript.js +1674 -64
  90. package/dist/extractors/javascript.js.map +1 -1
  91. package/dist/extractors/kotlin.js +5 -5
  92. package/dist/extractors/kotlin.js.map +1 -1
  93. package/dist/extractors/lua.js +1 -1
  94. package/dist/extractors/lua.js.map +1 -1
  95. package/dist/extractors/objc.js +3 -3
  96. package/dist/extractors/objc.js.map +1 -1
  97. package/dist/extractors/ocaml.js +1 -1
  98. package/dist/extractors/ocaml.js.map +1 -1
  99. package/dist/extractors/php.js +2 -2
  100. package/dist/extractors/php.js.map +1 -1
  101. package/dist/extractors/python.js +7 -7
  102. package/dist/extractors/python.js.map +1 -1
  103. package/dist/extractors/ruby.js +2 -2
  104. package/dist/extractors/ruby.js.map +1 -1
  105. package/dist/extractors/scala.js +1 -1
  106. package/dist/extractors/scala.js.map +1 -1
  107. package/dist/extractors/solidity.js +1 -1
  108. package/dist/extractors/solidity.js.map +1 -1
  109. package/dist/extractors/swift.js +4 -4
  110. package/dist/extractors/swift.js.map +1 -1
  111. package/dist/extractors/zig.js +4 -4
  112. package/dist/extractors/zig.js.map +1 -1
  113. package/dist/features/structure.d.ts.map +1 -1
  114. package/dist/features/structure.js +121 -16
  115. package/dist/features/structure.js.map +1 -1
  116. package/dist/infrastructure/config.d.ts +10 -0
  117. package/dist/infrastructure/config.d.ts.map +1 -1
  118. package/dist/infrastructure/config.js +15 -0
  119. package/dist/infrastructure/config.js.map +1 -1
  120. package/dist/infrastructure/native.d.ts +11 -0
  121. package/dist/infrastructure/native.d.ts.map +1 -1
  122. package/dist/infrastructure/native.js +78 -5
  123. package/dist/infrastructure/native.js.map +1 -1
  124. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  125. package/dist/presentation/queries-cli/overview.js +5 -0
  126. package/dist/presentation/queries-cli/overview.js.map +1 -1
  127. package/dist/types.d.ts +184 -0
  128. package/dist/types.d.ts.map +1 -1
  129. package/grammars/tree-sitter-erlang.wasm +0 -0
  130. package/package.json +9 -9
  131. package/src/db/migrations.ts +7 -0
  132. package/src/domain/analysis/module-map.ts +29 -1
  133. package/src/domain/graph/builder/call-resolver.ts +351 -0
  134. package/src/domain/graph/builder/cha.ts +175 -0
  135. package/src/domain/graph/builder/context.ts +3 -0
  136. package/src/domain/graph/builder/helpers.ts +175 -5
  137. package/src/domain/graph/builder/incremental.ts +186 -66
  138. package/src/domain/graph/builder/stages/build-edges.ts +1146 -146
  139. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  140. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  141. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  142. package/src/domain/graph/journal.ts +1 -1
  143. package/src/domain/graph/resolver/points-to.ts +254 -0
  144. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  145. package/src/domain/graph/watcher.ts +4 -2
  146. package/src/domain/parser.ts +43 -5
  147. package/src/domain/wasm-worker-entry.ts +25 -0
  148. package/src/domain/wasm-worker-pool.ts +21 -0
  149. package/src/domain/wasm-worker-protocol.ts +14 -0
  150. package/src/extractors/c.ts +3 -3
  151. package/src/extractors/clojure.ts +1 -1
  152. package/src/extractors/cpp.ts +3 -3
  153. package/src/extractors/csharp.ts +33 -9
  154. package/src/extractors/cuda.ts +3 -3
  155. package/src/extractors/elixir.ts +6 -6
  156. package/src/extractors/fsharp.ts +1 -1
  157. package/src/extractors/go.ts +5 -5
  158. package/src/extractors/haskell.ts +1 -1
  159. package/src/extractors/java.ts +2 -2
  160. package/src/extractors/javascript.ts +1802 -66
  161. package/src/extractors/kotlin.ts +5 -5
  162. package/src/extractors/lua.ts +1 -1
  163. package/src/extractors/objc.ts +3 -3
  164. package/src/extractors/ocaml.ts +1 -1
  165. package/src/extractors/php.ts +2 -2
  166. package/src/extractors/python.ts +7 -7
  167. package/src/extractors/ruby.ts +2 -2
  168. package/src/extractors/scala.ts +1 -1
  169. package/src/extractors/solidity.ts +1 -1
  170. package/src/extractors/swift.ts +4 -4
  171. package/src/extractors/zig.ts +4 -4
  172. package/src/features/structure.ts +143 -23
  173. package/src/infrastructure/config.ts +15 -0
  174. package/src/infrastructure/native.ts +87 -5
  175. package/src/presentation/queries-cli/overview.ts +15 -1
  176. package/src/types.ts +194 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optave/codegraph",
3
- "version": "3.11.1",
3
+ "version": "3.12.0",
4
4
  "description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -76,7 +76,7 @@
76
76
  "README.md"
77
77
  ],
78
78
  "engines": {
79
- "node": ">=22.6"
79
+ "node": ">=22.12.0"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsc && node -e \"require('fs').writeFileSync('dist/index.cjs',require('fs').readFileSync('src/index.cjs','utf8').replaceAll('./index.ts','./index.js'))\"",
@@ -119,7 +119,7 @@
119
119
  "homepage": "https://github.com/optave/ops-codegraph-tool#readme",
120
120
  "dependencies": {
121
121
  "better-sqlite3": "^12.6.2",
122
- "commander": "^14.0.3",
122
+ "commander": "^15.0.0",
123
123
  "web-tree-sitter": "^0.26.5"
124
124
  },
125
125
  "peerDependencies": {
@@ -132,12 +132,12 @@
132
132
  },
133
133
  "optionalDependencies": {
134
134
  "@modelcontextprotocol/sdk": "^1.0.0",
135
- "@optave/codegraph-darwin-arm64": "3.11.1",
136
- "@optave/codegraph-darwin-x64": "3.11.1",
137
- "@optave/codegraph-linux-arm64-gnu": "3.11.1",
138
- "@optave/codegraph-linux-x64-gnu": "3.11.1",
139
- "@optave/codegraph-linux-x64-musl": "3.11.1",
140
- "@optave/codegraph-win32-x64-msvc": "3.11.1"
135
+ "@optave/codegraph-darwin-arm64": "3.12.0",
136
+ "@optave/codegraph-darwin-x64": "3.12.0",
137
+ "@optave/codegraph-linux-arm64-gnu": "3.12.0",
138
+ "@optave/codegraph-linux-x64-gnu": "3.12.0",
139
+ "@optave/codegraph-linux-x64-musl": "3.12.0",
140
+ "@optave/codegraph-win32-x64-msvc": "3.12.0"
141
141
  },
142
142
  "devDependencies": {
143
143
  "@biomejs/biome": "^2.4.4",
@@ -256,6 +256,13 @@ export const MIGRATIONS: Migration[] = [
256
256
  CREATE INDEX IF NOT EXISTS idx_edges_kind_source ON edges(kind, source_id);
257
257
  `,
258
258
  },
259
+ {
260
+ version: 17,
261
+ up: `
262
+ ALTER TABLE edges ADD COLUMN technique TEXT;
263
+ CREATE INDEX IF NOT EXISTS idx_edges_technique ON edges(technique);
264
+ `,
265
+ },
259
266
  ];
260
267
 
261
268
  interface PragmaColumnInfo {
@@ -163,6 +163,26 @@ function getEmbeddingsInfo(db: BetterSqlite3Database) {
163
163
  return null;
164
164
  }
165
165
 
166
+ function countCallEdgesByTechnique(
167
+ db: BetterSqlite3Database,
168
+ testFilter: string,
169
+ ): Record<string, number> {
170
+ // testFilter uses n.file — join source node to apply the same file-scope as
171
+ // the rest of computeQualityMetrics so --no-tests is consistent.
172
+ const rows = db
173
+ .prepare(
174
+ `SELECT e.technique, COUNT(*) as c
175
+ FROM edges e
176
+ JOIN nodes n ON e.source_id = n.id
177
+ WHERE e.kind = 'calls' AND e.technique IS NOT NULL ${testFilter}
178
+ GROUP BY e.technique`,
179
+ )
180
+ .all() as Array<{ technique: string; c: number }>;
181
+ const byTechnique: Record<string, number> = {};
182
+ for (const r of rows) byTechnique[r.technique] = r.c;
183
+ return byTechnique;
184
+ }
185
+
166
186
  function computeQualityMetrics(
167
187
  db: BetterSqlite3Database,
168
188
  testFilter: string,
@@ -205,13 +225,16 @@ function computeQualityMetrics(
205
225
  const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
206
226
 
207
227
  const score = computeQualityScore(callerCoverage, callConfidence, falsePositiveRatio);
228
+ const byTechnique = countCallEdgesByTechnique(db, testFilter);
208
229
 
209
230
  return {
210
231
  score,
211
232
  callerCoverage: {
212
233
  ratio: callerCoverage,
234
+ percentage: Math.round(callerCoverage * 100),
213
235
  covered: callableWithCallers,
214
236
  total: totalCallable,
237
+ byTechnique: Object.keys(byTechnique).length > 0 ? byTechnique : undefined,
215
238
  },
216
239
  callConfidence: {
217
240
  ratio: callConfidence,
@@ -388,6 +411,7 @@ function buildStatsFromNative(
388
411
  db: BetterSqlite3Database,
389
412
  nativeStats: NativeGraphStats,
390
413
  config: any,
414
+ noTests: boolean,
391
415
  jsSections: {
392
416
  files: ReturnType<typeof countFilesByLanguage>;
393
417
  fileCycles: unknown[];
@@ -413,6 +437,8 @@ function buildStatsFromNative(
413
437
  for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
414
438
  const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
415
439
  const score = computeQualityScore(callerCoverage, callConfidence, falsePositiveRatio);
440
+ const testFilter = testFilterSQL('n.file', noTests);
441
+ const byTechnique = countCallEdgesByTechnique(db, testFilter);
416
442
 
417
443
  return {
418
444
  nodes: { total: s.totalNodes, byKind: nodesByKind },
@@ -432,8 +458,10 @@ function buildStatsFromNative(
432
458
  score,
433
459
  callerCoverage: {
434
460
  ratio: callerCoverage,
461
+ percentage: Math.round(callerCoverage * 100),
435
462
  covered: s.quality.callableWithCallers,
436
463
  total: s.quality.callableTotal,
464
+ byTechnique: Object.keys(byTechnique).length > 0 ? byTechnique : undefined,
437
465
  },
438
466
  callConfidence: {
439
467
  ratio: callConfidence,
@@ -508,7 +536,7 @@ export function statsData(customDbPath: string, opts: { noTests?: boolean; confi
508
536
 
509
537
  const nativeStats = nativeDb?.getGraphStats?.(noTests);
510
538
  return nativeStats
511
- ? buildStatsFromNative(db, nativeStats, config, jsSections)
539
+ ? buildStatsFromNative(db, nativeStats, config, noTests, jsSections)
512
540
  : buildStatsFromJs(db, noTests, config, jsSections);
513
541
  } finally {
514
542
  close();
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Shared call-edge resolution — used by both the full build pipeline
3
+ * (build-edges.ts) and the incremental watch path (incremental.ts).
4
+ *
5
+ * Both callers supply a `CallNodeLookup` adapter that abstracts their
6
+ * node-lookup mechanism (pre-loaded Maps vs. per-query SQLite statements).
7
+ * The resolution logic lives here exactly once.
8
+ */
9
+ import { computeConfidence } from '../resolve.js';
10
+
11
+ // ── Public interface ─────────────────────────────────────────────────────
12
+
13
+ export interface CallNodeLookup {
14
+ byNameAndFile(
15
+ name: string,
16
+ file: string,
17
+ ): ReadonlyArray<{ id: number; file: string; kind?: string }>;
18
+ byName(name: string): ReadonlyArray<{ id: number; file: string; kind?: string }>;
19
+ isBarrel(file: string): boolean;
20
+ resolveBarrel(barrelFile: string, symbolName: string): string | null;
21
+ nodeId(name: string, kind: string, file: string, line: number): { id: number } | undefined;
22
+ }
23
+
24
+ export const RECEIVER_KINDS = new Set(['class', 'struct', 'interface', 'type', 'module']);
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
+
48
+ // ── Shared resolution functions ──────────────────────────────────────────
49
+
50
+ export function findCaller(
51
+ lookup: CallNodeLookup,
52
+ call: { line: number },
53
+ definitions: ReadonlyArray<{
54
+ name: string;
55
+ kind: string;
56
+ line: number;
57
+ endLine?: number | null;
58
+ }>,
59
+ relPath: string,
60
+ fileNodeRow: { id: number },
61
+ ): { id: number; callerName: string | null } {
62
+ let caller: { id: number } | null = null;
63
+ let callerName: string | null = null;
64
+ let callerSpan = Infinity;
65
+ for (const def of definitions) {
66
+ if (def.line <= call.line) {
67
+ const end = def.endLine || Infinity;
68
+ if (call.line <= end) {
69
+ const span = end - def.line;
70
+ if (span < callerSpan) {
71
+ const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
72
+ if (row) {
73
+ caller = row;
74
+ callerName = def.name;
75
+ callerSpan = span;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return { ...(caller ?? fileNodeRow), callerName };
82
+ }
83
+
84
+ export function resolveByMethodOrGlobal(
85
+ lookup: CallNodeLookup,
86
+ call: { name: string; receiver?: string | null },
87
+ relPath: string,
88
+ typeMap: Map<string, unknown>,
89
+ callerName?: string | null,
90
+ ): ReadonlyArray<{ id: number; file: string }> {
91
+ if (call.receiver) {
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
114
+ ? typeof typeEntry === 'string'
115
+ ? typeEntry
116
+ : (typeEntry as { type?: string }).type
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
+
133
+ if (typeName) {
134
+ const typed = lookup
135
+ .byName(`${typeName}.${call.name}`)
136
+ .filter((n) => n.kind === 'method' && computeConfidence(relPath, n.file, null) >= 0.5);
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;
184
+ }
185
+ }
186
+ if (
187
+ !call.receiver ||
188
+ call.receiver === 'this' ||
189
+ call.receiver === 'self' ||
190
+ call.receiver === 'super'
191
+ ) {
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
254
+ }
255
+ return [];
256
+ }
257
+
258
+ export function resolveCallTargets(
259
+ lookup: CallNodeLookup,
260
+ call: { name: string; receiver?: string | null },
261
+ relPath: string,
262
+ importedNames: Map<string, string>,
263
+ typeMap: Map<string, unknown>,
264
+ callerName?: string | null,
265
+ ): { targets: Array<{ id: number; file: string }>; importedFrom: string | undefined } {
266
+ const importedFrom = importedNames.get(call.name);
267
+ let targets: ReadonlyArray<{ id: number; file: string }> | undefined;
268
+
269
+ if (importedFrom) {
270
+ targets = lookup.byNameAndFile(call.name, importedFrom);
271
+ if (targets.length === 0 && lookup.isBarrel(importedFrom)) {
272
+ const actualSource = lookup.resolveBarrel(importedFrom, call.name);
273
+ if (actualSource) {
274
+ targets = lookup.byNameAndFile(call.name, actualSource);
275
+ }
276
+ }
277
+ }
278
+
279
+ if (!targets || targets.length === 0) {
280
+ targets = lookup.byNameAndFile(call.name, relPath);
281
+ if (targets.length === 0) {
282
+ targets = resolveByMethodOrGlobal(lookup, call, relPath, typeMap, callerName);
283
+ }
284
+ }
285
+
286
+ const resolved = [...(targets ?? [])];
287
+ if (resolved.length > 1) {
288
+ resolved.sort((a, b) => {
289
+ const confA = computeConfidence(relPath, a.file, importedFrom ?? null);
290
+ const confB = computeConfidence(relPath, b.file, importedFrom ?? null);
291
+ return confB - confA;
292
+ });
293
+ }
294
+ return { targets: resolved, importedFrom };
295
+ }
296
+
297
+ /**
298
+ * Resolve the receiver-type edge for a call site.
299
+ * Returns the edge tuple to insert, or null if nothing matched or the edge
300
+ * was already seen. Callers are responsible for the actual DB/array insert.
301
+ *
302
+ * Receiver resolution collects all same-file candidates first (no kind
303
+ * filter), falls back to global candidates only when the same-file set is
304
+ * entirely empty, then filters the chosen set by RECEIVER_KINDS. This
305
+ * matches the native Rust build path: if a file imports a name that happens
306
+ * to be emitted as `kind='function'` in the importer, the same-file set is
307
+ * non-empty and blocks the global fallback, so no receiver edge is emitted.
308
+ * Keeping this behaviour identical to the Rust path maintains engine parity.
309
+ */
310
+ export function resolveReceiverEdge(
311
+ lookup: CallNodeLookup,
312
+ call: { name: string; receiver: string },
313
+ caller: { id: number },
314
+ relPath: string,
315
+ typeMap: Map<string, unknown>,
316
+ seenCallEdges: Set<string>,
317
+ ): { callerId: number; receiverId: number; confidence: number } | null {
318
+ const typeEntry = typeMap.get(call.receiver);
319
+ const typeName = typeEntry
320
+ ? typeof typeEntry === 'string'
321
+ ? typeEntry
322
+ : ((typeEntry as { type?: string }).type ?? null)
323
+ : null;
324
+ const typeConfidence =
325
+ typeEntry && typeof typeEntry !== 'string'
326
+ ? ((typeEntry as { confidence?: number }).confidence ?? null)
327
+ : null;
328
+ const effectiveReceiver = typeName || call.receiver;
329
+ // Filter-before: apply RECEIVER_KINDS to same-file candidates first, then
330
+ // fall back to global candidates (also filtered) only when same-file yields
331
+ // nothing. This prevents an imported name emitted as kind='function' in the
332
+ // importing file from blocking the fallback to the actual class/struct/etc.
333
+ // node in the defining file.
334
+ const sameFileCandidates = lookup
335
+ .byNameAndFile(effectiveReceiver, relPath)
336
+ .filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
337
+ const candidates =
338
+ sameFileCandidates.length > 0
339
+ ? sameFileCandidates
340
+ : lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
341
+ if (candidates.length === 0) return null;
342
+ const recvTarget = candidates[0]!;
343
+ const recvKey = `recv|${caller.id}|${recvTarget.id}`;
344
+ if (seenCallEdges.has(recvKey)) return null;
345
+ seenCallEdges.add(recvKey);
346
+ return {
347
+ callerId: caller.id,
348
+ receiverId: recvTarget.id,
349
+ confidence: typeConfidence ?? (typeName ? 0.9 : 0.7),
350
+ };
351
+ }
@@ -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 ─────────────────────────────────────────────────────