@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
@@ -0,0 +1,536 @@
1
+ /**
2
+ * TypeScript-native type resolver (Phase 8.1).
3
+ *
4
+ * Runs as a build-time enrichment pass after tree-sitter parsing. Uses the
5
+ * TypeScript compiler API to resolve the actual runtime type of every variable
6
+ * and parameter in .ts/.tsx files, replacing heuristic typeMap entries (0.7–0.9
7
+ * confidence) with compiler-verified ones (1.0).
8
+ *
9
+ * Tree-sitter parses fast; this pass resolves accurately. Together they give
10
+ * codegraph both speed and precision on its primary use case.
11
+ *
12
+ * The `typescript` package is a peer/optional dependency — it is present on any
13
+ * machine that compiles TypeScript but is not bundled with codegraph itself. This
14
+ * module lazy-imports it at runtime; if the import fails the pass is silently
15
+ * skipped so JS-only projects and environments without `typescript` installed are
16
+ * unaffected.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { debug } from '../../../infrastructure/logger.js';
21
+ import type { CallAssignment, ExtractorOutput, TypeMapEntry } from '../../../types.js';
22
+
23
+ // typescript is not a hard dependency — lazy-load it so JS-only projects
24
+ // and environments without typescript installed work without error.
25
+ type TsModule = typeof import('typescript');
26
+ let _ts: TsModule | null | undefined; // undefined = not yet tried; null = unavailable
27
+
28
+ async function loadTs(): Promise<TsModule | null> {
29
+ if (_ts !== undefined) return _ts;
30
+ try {
31
+ // TypeScript 6+ ships dual CJS/ESM exports; `.default` is the CJS interop
32
+ // namespace and is present and non-null in both TS 5.x and TS 6.x.
33
+ _ts = (await import('typescript')).default as TsModule;
34
+ } catch {
35
+ _ts = null;
36
+ debug('ts-resolver: typescript package not available — skipping TSC type enrichment');
37
+ }
38
+ return _ts;
39
+ }
40
+
41
+ const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
42
+
43
+ function isTsFile(relPath: string): boolean {
44
+ // Exclude .d.ts declaration files — path.extname('.d.ts') returns '.ts',
45
+ // so we must check the full suffix explicitly.
46
+ return TS_EXTENSIONS.has(path.extname(relPath)) && !relPath.endsWith('.d.ts');
47
+ }
48
+
49
+ // Primitive and built-in type names that don't help call resolution.
50
+ const SKIP_TYPE_NAMES = new Set([
51
+ 'string',
52
+ 'number',
53
+ 'boolean',
54
+ 'any',
55
+ 'unknown',
56
+ 'never',
57
+ 'void',
58
+ 'null',
59
+ 'undefined',
60
+ 'object',
61
+ 'symbol',
62
+ 'bigint',
63
+ 'String',
64
+ 'Number',
65
+ 'Boolean',
66
+ 'Object',
67
+ 'Array',
68
+ 'Promise',
69
+ 'Map',
70
+ 'Set',
71
+ 'WeakMap',
72
+ 'WeakSet',
73
+ 'Error',
74
+ 'Function',
75
+ 'RegExp',
76
+ 'Date',
77
+ ]);
78
+
79
+ /**
80
+ * Enrich the typeMap for every .ts/.tsx file using the TypeScript compiler API.
81
+ *
82
+ * Called from buildEdges before call-edge construction. Only overwrites entries
83
+ * with lower confidence than 1.0 (constructor calls are already exact).
84
+ */
85
+ export async function enrichTypeMapWithTsc(
86
+ rootDir: string,
87
+ fileSymbols: Map<string, ExtractorOutput>,
88
+ ): Promise<void> {
89
+ const tsRelPaths = [...fileSymbols.keys()].filter(isTsFile);
90
+ if (tsRelPaths.length === 0) return;
91
+
92
+ const ts = await loadTs();
93
+ if (!ts) return;
94
+
95
+ const tsconfigPath = findTsconfig(rootDir);
96
+ if (!tsconfigPath) {
97
+ debug('ts-resolver: no tsconfig.json found — skipping TypeScript type enrichment');
98
+ return;
99
+ }
100
+
101
+ const t0 = Date.now();
102
+ const program = createProgram(ts, tsconfigPath);
103
+ if (!program) return;
104
+
105
+ const checker = program.getTypeChecker();
106
+ let enrichedFiles = 0;
107
+ let enrichedEntries = 0;
108
+ let backfilledFiles = 0;
109
+
110
+ for (const relPath of tsRelPaths) {
111
+ const symbols = fileSymbols.get(relPath)!;
112
+ const absPath = path.resolve(rootDir, relPath);
113
+ const sourceFile = program.getSourceFile(absPath);
114
+ if (!sourceFile) continue;
115
+
116
+ const before = symbols.typeMap.size;
117
+ const countBefore = countLowConfidence(symbols.typeMap);
118
+ enrichSourceFile(ts, sourceFile, checker, symbols.typeMap);
119
+ const countAfter = countLowConfidence(symbols.typeMap);
120
+ const gained = countBefore - countAfter + (symbols.typeMap.size - before);
121
+ if (gained > 0) {
122
+ enrichedEntries += gained;
123
+ enrichedFiles++;
124
+ }
125
+
126
+ // Phase 8.2 parity: backfill returnTypeMap and callAssignments for engines
127
+ // (native Rust) that don't populate them during extraction. The JS extractor
128
+ // sets these fields; native leaves them undefined.
129
+ // Guards are intentionally independent so a future extractor that sets one
130
+ // but not the other is handled correctly without silently skipping either.
131
+ let didBackfill = false;
132
+ if (symbols.returnTypeMap === undefined) {
133
+ symbols.returnTypeMap = new Map();
134
+ enrichReturnTypeMap(ts, sourceFile, checker, symbols.returnTypeMap);
135
+ if (symbols.returnTypeMap.size > 0) didBackfill = true;
136
+ }
137
+ if (symbols.callAssignments === undefined) {
138
+ symbols.callAssignments = [];
139
+ enrichCallAssignments(ts, sourceFile, symbols.typeMap, symbols.callAssignments);
140
+ if (symbols.callAssignments.length > 0) didBackfill = true;
141
+ }
142
+ if (didBackfill) backfilledFiles++;
143
+ }
144
+
145
+ debug(
146
+ `ts-resolver: enriched ${enrichedEntries} typeMap entries across ${enrichedFiles} files` +
147
+ (backfilledFiles > 0
148
+ ? `, backfilled returnTypeMap/callAssignments in ${backfilledFiles} files`
149
+ : '') +
150
+ ` in ${Date.now() - t0}ms`,
151
+ );
152
+ }
153
+
154
+ function countLowConfidence(typeMap: Map<string, TypeMapEntry>): number {
155
+ let count = 0;
156
+ for (const entry of typeMap.values()) {
157
+ if (entry.confidence < 1.0) count++;
158
+ }
159
+ return count;
160
+ }
161
+
162
+ /**
163
+ * Walk up from rootDir looking for tsconfig.json (up to 4 levels).
164
+ * Handles monorepo setups where rootDir is a package subdirectory but
165
+ * the tsconfig lives at the repository root.
166
+ */
167
+ function findTsconfig(rootDir: string): string | null {
168
+ let dir = rootDir;
169
+ for (let i = 0; i < 4; i++) {
170
+ const candidate = path.join(dir, 'tsconfig.json');
171
+ if (fs.existsSync(candidate)) return candidate;
172
+ const parent = path.dirname(dir);
173
+ if (parent === dir) break; // reached filesystem root
174
+ dir = parent;
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function createProgram(ts: TsModule, tsconfigPath: string): import('typescript').Program | null {
180
+ try {
181
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
182
+ if (configFile.error) {
183
+ debug(
184
+ `ts-resolver: tsconfig error — ${ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n')}`,
185
+ );
186
+ return null;
187
+ }
188
+
189
+ const parsed = ts.parseJsonConfigFileContent(
190
+ configFile.config,
191
+ ts.sys,
192
+ path.dirname(tsconfigPath),
193
+ );
194
+
195
+ if (parsed.errors.length > 0) {
196
+ for (const err of parsed.errors) {
197
+ debug(
198
+ `ts-resolver: tsconfig parse warning — ${ts.flattenDiagnosticMessageText(err.messageText, '\n')}`,
199
+ );
200
+ }
201
+ }
202
+
203
+ if (parsed.fileNames.length === 0) {
204
+ // Empty fileNames usually means a solution-style tsconfig that only has
205
+ // `references:[]` and no `files`/`include`. In this case ts.createProgram
206
+ // would receive [tsconfigPath] as source — a JSON file — and every
207
+ // subsequent getSourceFile() call for real .ts files returns undefined,
208
+ // producing zero enrichment silently. Warn instead of wasting time.
209
+ debug(
210
+ 'ts-resolver: tsconfig resolved no source files (solution-style tsconfig?) — skipping enrichment',
211
+ );
212
+ return null;
213
+ }
214
+
215
+ return ts.createProgram({
216
+ rootNames: parsed.fileNames,
217
+ options: {
218
+ ...parsed.options,
219
+ noEmit: true,
220
+ skipLibCheck: true,
221
+ },
222
+ });
223
+ } catch (err) {
224
+ debug(`ts-resolver: failed to create TS program — ${err}`);
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Walk a single SourceFile and update typeMap entries for:
231
+ * - Variable declarations: const/let/var names with inferred or annotated types
232
+ * - Function/method parameters with type annotations
233
+ *
234
+ * Keys are scoped as `<line>:<col>:<name>` to avoid collisions across functions
235
+ * that share parameter names (e.g., two functions both taking `service`). The
236
+ * call-edge resolver looks up by bare name, so we only write bare-name entries
237
+ * when there is no ambiguity (i.e., the name appears exactly once in this file).
238
+ *
239
+ * Entries already at confidence 1.0 (e.g., `new Foo()` from tree-sitter) are
240
+ * left unchanged. New entries from the compiler are added at confidence 1.0.
241
+ */
242
+ function enrichSourceFile(
243
+ ts: TsModule,
244
+ sourceFile: import('typescript').SourceFile,
245
+ checker: import('typescript').TypeChecker,
246
+ typeMap: Map<string, TypeMapEntry>,
247
+ ): void {
248
+ // First pass: collect resolved types keyed by bare identifier name.
249
+ // Track both the short name (for typeMap writes) and the fully-qualified name
250
+ // (module-path-prefixed) for ambiguity detection. Two classes may share the
251
+ // same short name (e.g., `OrderService` from two different modules), and
252
+ // symbol.getName() returns the declared name — not the local alias — so
253
+ // deduplication on short names alone would incorrectly collapse them.
254
+ const nameToEntries = new Map<string, { shortName: string; qualifiedName: string }[]>();
255
+ // Track class property declaration names so we can also seed "this.X" entries.
256
+ const propertyDeclNames = new Set<string>();
257
+
258
+ function visit(node: import('typescript').Node): void {
259
+ let identName: string | null = null;
260
+ let nameNode: import('typescript').Identifier | null = null;
261
+
262
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
263
+ identName = node.name.text;
264
+ nameNode = node.name;
265
+ } else if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
266
+ identName = node.name.text;
267
+ nameNode = node.name;
268
+ } else if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name)) {
269
+ // TypeScript class field: `private repo: Repository<User>`
270
+ // Seeds typeMap so `this.repo.method()` can be resolved via receiver type.
271
+ identName = node.name.text;
272
+ nameNode = node.name;
273
+ propertyDeclNames.add(node.name.text);
274
+ }
275
+
276
+ if (identName && nameNode) {
277
+ const resolved = resolveTypeName(ts, nameNode, checker);
278
+ if (resolved) {
279
+ const existing = nameToEntries.get(identName);
280
+ if (existing) {
281
+ existing.push(resolved);
282
+ } else {
283
+ nameToEntries.set(identName, [resolved]);
284
+ }
285
+ }
286
+ }
287
+
288
+ ts.forEachChild(node, visit);
289
+ }
290
+ ts.forEachChild(sourceFile, visit);
291
+
292
+ // Second pass: only write unambiguous entries (single unique qualified type for a name)
293
+ for (const [name, entries] of nameToEntries) {
294
+ const uniqueQualified = [...new Set(entries.map((e) => e.qualifiedName))];
295
+ if (uniqueQualified.length !== 1) continue; // ambiguous across modules — skip
296
+ // entries is non-empty because we only set() on first occurrence and push() after —
297
+ // TypeScript's noUncheckedIndexedAccess can flag [0] access, so assert the type.
298
+ const first = entries[0];
299
+ if (!first) continue;
300
+ const shortName = first.shortName;
301
+ const existing = typeMap.get(name);
302
+ if (!existing || existing.confidence < 1.0) {
303
+ typeMap.set(name, { type: shortName, confidence: 1.0 });
304
+ }
305
+ // For class property declarations, also seed "this.fieldName" so that
306
+ // `this.repo.findById()` call sites resolve to the interface/class type.
307
+ if (propertyDeclNames.has(name)) {
308
+ const thisKey = `this.${name}`;
309
+ const existingThis = typeMap.get(thisKey);
310
+ if (!existingThis || existingThis.confidence < 1.0) {
311
+ typeMap.set(thisKey, { type: shortName, confidence: 1.0 });
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Walk a SourceFile and populate returnTypeMap with compiler-verified return types.
319
+ * Handles function declarations, method declarations, and arrow/function-expression
320
+ * variable initialisers at module scope. Methods are stored as `ClassName.methodName`.
321
+ *
322
+ * Only captures declarations at module scope or directly inside a class body —
323
+ * local functions nested inside method bodies are excluded to avoid spurious
324
+ * cross-file type matches (same guard as enrichSourceFile's "unambiguous names only"
325
+ * heuristic). Recursion stops at function/method body boundaries.
326
+ *
327
+ * Async functions returning Promise<T> are unwrapped: the inner type argument T is
328
+ * used so that async methods receive a returnTypeMap entry just like sync ones.
329
+ */
330
+ function enrichReturnTypeMap(
331
+ ts: TsModule,
332
+ sourceFile: import('typescript').SourceFile,
333
+ checker: import('typescript').TypeChecker,
334
+ returnTypeMap: Map<string, TypeMapEntry>,
335
+ ): void {
336
+ let currentClass: string | null = null;
337
+
338
+ /**
339
+ * Resolve the concrete return type name for a signature, unwrapping
340
+ * Promise<T> so async functions contribute their inner type.
341
+ */
342
+ function resolveReturnTypeName(sig: import('typescript').Signature | undefined): string | null {
343
+ if (!sig) return null;
344
+ try {
345
+ let retType = checker.getReturnTypeOfSignature(sig);
346
+
347
+ // Unwrap Promise<T> → T so async functions get a useful returnTypeMap entry.
348
+ const outerSym = retType.getSymbol() ?? retType.aliasSymbol;
349
+ if (outerSym?.getName() === 'Promise') {
350
+ const args = checker.getTypeArguments(retType as import('typescript').TypeReference);
351
+ if (args.length > 0) retType = args[0]!;
352
+ }
353
+
354
+ const sym = retType.getSymbol() ?? retType.aliasSymbol;
355
+ if (!sym) return null;
356
+ const name = sym.getName();
357
+ if (!name || name === '__type' || name === '__object' || SKIP_TYPE_NAMES.has(name))
358
+ return null;
359
+ return name;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
365
+ function writeEntry(fnName: string, sigNode: import('typescript').SignatureDeclaration): void {
366
+ const typeName = resolveReturnTypeName(checker.getSignatureFromDeclaration(sigNode));
367
+ if (typeName) {
368
+ const existing = returnTypeMap.get(fnName);
369
+ if (!existing || existing.confidence < 1.0)
370
+ returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Visit nodes at the current lexical scope (module level or class body).
376
+ * Does NOT recurse into function/method bodies to avoid capturing local
377
+ * helper functions under bare names.
378
+ */
379
+ function visit(node: import('typescript').Node): void {
380
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
381
+ // Enter class scope: visit direct children (method/property declarations).
382
+ const saved = currentClass;
383
+ currentClass =
384
+ (node as import('typescript').ClassDeclaration | import('typescript').ClassExpression).name
385
+ ?.text ?? null;
386
+ ts.forEachChild(node, visit);
387
+ currentClass = saved;
388
+ return; // class body fully handled — stop here
389
+ }
390
+
391
+ if (ts.isFunctionDeclaration(node) && node.name) {
392
+ // Module-level function declaration: record and stop (no body descent).
393
+ writeEntry(node.name.text, node);
394
+ return;
395
+ }
396
+
397
+ if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
398
+ // Class method: record as ClassName.methodName and stop.
399
+ const fnName = currentClass ? `${currentClass}.${node.name.text}` : node.name.text;
400
+ writeEntry(fnName, node);
401
+ return;
402
+ }
403
+
404
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
405
+ // Arrow/function-expression assigned to a variable at the current scope.
406
+ // Because we never recurse into function bodies, any VariableDeclaration
407
+ // we see here is guaranteed to be at module scope or inside a class body
408
+ // (not inside a method body), making the bare name safe for cross-file use.
409
+ const init = node.initializer;
410
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
411
+ writeEntry(node.name.text, init);
412
+ }
413
+ return; // variable declaration fully handled — stop here
414
+ }
415
+
416
+ // For all other node kinds (VariableStatement, VariableDeclarationList,
417
+ // ExportDeclaration, etc.) recurse to reach nested function/class/var nodes.
418
+ ts.forEachChild(node, visit);
419
+ }
420
+
421
+ ts.forEachChild(sourceFile, visit);
422
+ }
423
+
424
+ /**
425
+ * Walk a SourceFile and push call assignments (`const x = fn()`) whose variable
426
+ * is not yet in typeMap into callAssignments for cross-file propagation.
427
+ * Phase 8.1 already resolved the common case into typeMap; this captures the rest.
428
+ *
429
+ * Uses the same two-pass "unambiguous names only" strategy as `enrichSourceFile`:
430
+ * collect all candidates first, then only push entries where a given `varName`
431
+ * maps to exactly one distinct `calleeName`. This prevents multiple methods in the
432
+ * same file that each bind a different imported function to a common local name
433
+ * (e.g., `const result = getA()` in one method, `const result = getB()` in
434
+ * another) from both landing in `callAssignments`, which would cause
435
+ * `propagateReturnTypesAcrossFiles` to silently resolve one arbitrarily.
436
+ */
437
+ function enrichCallAssignments(
438
+ ts: TsModule,
439
+ sourceFile: import('typescript').SourceFile,
440
+ typeMap: Map<string, TypeMapEntry>,
441
+ callAssignments: CallAssignment[],
442
+ ): void {
443
+ // First pass: collect all candidates keyed by varName.
444
+ const candidates = new Map<string, CallAssignment[]>();
445
+
446
+ function visit(node: import('typescript').Node): void {
447
+ if (
448
+ ts.isVariableDeclaration(node) &&
449
+ ts.isIdentifier(node.name) &&
450
+ node.initializer &&
451
+ ts.isCallExpression(node.initializer)
452
+ ) {
453
+ const varName = node.name.text;
454
+ if (!typeMap.has(varName)) {
455
+ const call = node.initializer;
456
+ let calleeName: string | null = null;
457
+ let receiverTypeName: string | undefined;
458
+
459
+ if (ts.isIdentifier(call.expression)) {
460
+ calleeName = call.expression.text;
461
+ } else if (ts.isPropertyAccessExpression(call.expression)) {
462
+ calleeName = call.expression.name.text;
463
+ const obj = call.expression.expression;
464
+ if (ts.isIdentifier(obj)) {
465
+ const entry = typeMap.get(obj.text);
466
+ if (entry && typeof entry === 'object') receiverTypeName = entry.type;
467
+ }
468
+ }
469
+
470
+ if (calleeName) {
471
+ const ca: CallAssignment = { varName, calleeName, receiverTypeName };
472
+ const existing = candidates.get(varName);
473
+ if (existing) {
474
+ existing.push(ca);
475
+ } else {
476
+ candidates.set(varName, [ca]);
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ ts.forEachChild(node, visit);
483
+ }
484
+
485
+ ts.forEachChild(sourceFile, visit);
486
+
487
+ // Second pass: only push entries where varName maps to exactly one distinct
488
+ // calleeName. Ambiguous varNames (same name, different callees across scopes)
489
+ // are excluded to avoid silently resolving the wrong type cross-file.
490
+ for (const entries of candidates.values()) {
491
+ const uniqueCallees = new Set(entries.map((e) => e.calleeName));
492
+ if (uniqueCallees.size === 1) {
493
+ callAssignments.push(entries[0] as CallAssignment);
494
+ }
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Ask the type checker for the type of a name node and return both the short
500
+ * declared name and the fully-qualified module-prefixed name. Returns null when
501
+ * the type is a primitive, anonymous, or otherwise not useful for resolution.
502
+ *
503
+ * The fully-qualified name (e.g., `"./legacy/service".OrderService`) is used for
504
+ * ambiguity detection — it distinguishes two classes that share the same short
505
+ * declaration name but come from different modules. The short name is what the
506
+ * call-edge resolver looks up in the typeMap.
507
+ */
508
+ function resolveTypeName(
509
+ ts: TsModule,
510
+ nameNode: import('typescript').Identifier,
511
+ checker: import('typescript').TypeChecker,
512
+ ): { shortName: string; qualifiedName: string } | null {
513
+ try {
514
+ const type = checker.getTypeAtLocation(nameNode);
515
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
516
+ if (!symbol) return null;
517
+ const shortName = symbol.getName();
518
+ if (
519
+ !shortName ||
520
+ shortName === '__type' ||
521
+ shortName === '__object' ||
522
+ SKIP_TYPE_NAMES.has(shortName) ||
523
+ // Skip generic type-parameter symbols (T, E, K, etc.) — they do not
524
+ // correspond to any real class and would overwrite useful lower-confidence
525
+ // heuristic entries, causing call edges to be silently dropped.
526
+ (symbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeAlias)) !== 0
527
+ )
528
+ return null;
529
+ // getFullyQualifiedName returns e.g. `"./path/to/module".ClassName` for
530
+ // imported symbols — unique across modules even when short names collide.
531
+ const qualifiedName = checker.getFullyQualifiedName(symbol);
532
+ return { shortName, qualifiedName };
533
+ } catch {
534
+ return null;
535
+ }
536
+ }