@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.
- package/README.md +73 -37
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +8 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts +2 -0
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +24 -2
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +251 -34
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +69 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +158 -0
- package/dist/domain/graph/builder/cha.js.map +1 -0
- package/dist/domain/graph/builder/context.d.ts +3 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +2 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +25 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +178 -5
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +74 -2
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +704 -34
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/journal.js +1 -1
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts +53 -0
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
- package/dist/domain/graph/resolver/points-to.js +213 -0
- package/dist/domain/graph/resolver/points-to.js.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.js +476 -0
- package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
- package/dist/domain/parser.d.ts +12 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +83 -20
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +35 -2
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +34 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +15 -1
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/c.js +3 -3
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/clojure.js +1 -1
- package/dist/extractors/clojure.js.map +1 -1
- package/dist/extractors/cpp.d.ts.map +1 -1
- package/dist/extractors/cpp.js +45 -4
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/csharp.d.ts.map +1 -1
- package/dist/extractors/csharp.js +37 -8
- package/dist/extractors/csharp.js.map +1 -1
- package/dist/extractors/cuda.d.ts.map +1 -1
- package/dist/extractors/cuda.js +45 -4
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +6 -6
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/fsharp.js +1 -1
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/go.js +5 -5
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/haskell.js +1 -1
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +10 -9
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts +2 -0
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +1812 -71
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +5 -5
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/lua.js +1 -1
- package/dist/extractors/lua.js.map +1 -1
- package/dist/extractors/objc.js +3 -3
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/ocaml.js +1 -1
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/php.js +2 -2
- package/dist/extractors/php.js.map +1 -1
- package/dist/extractors/python.js +7 -7
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/ruby.js +2 -2
- package/dist/extractors/ruby.js.map +1 -1
- package/dist/extractors/scala.js +1 -1
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +1 -1
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +4 -4
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +4 -4
- package/dist/extractors/zig.js.map +1 -1
- package/dist/features/structure-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +85 -2
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +408 -19
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/native.d.ts +11 -0
- package/dist/infrastructure/native.d.ts.map +1 -1
- package/dist/infrastructure/native.js +78 -5
- package/dist/infrastructure/native.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +5 -0
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +221 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +8 -1
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +263 -35
- package/src/domain/graph/builder/cha.ts +192 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +195 -5
- package/src/domain/graph/builder/incremental.ts +80 -1
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +867 -32
- package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
- package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
- package/src/domain/graph/journal.ts +1 -1
- package/src/domain/graph/resolver/points-to.ts +254 -0
- package/src/domain/graph/resolver/ts-resolver.ts +536 -0
- package/src/domain/parser.ts +86 -17
- package/src/domain/wasm-worker-entry.ts +35 -2
- package/src/domain/wasm-worker-pool.ts +22 -0
- package/src/domain/wasm-worker-protocol.ts +15 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +47 -4
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +47 -4
- package/src/extractors/elixir.ts +6 -6
- package/src/extractors/fsharp.ts +1 -1
- package/src/extractors/go.ts +5 -5
- package/src/extractors/haskell.ts +1 -1
- package/src/extractors/helpers.ts +43 -0
- package/src/extractors/java.ts +10 -9
- package/src/extractors/javascript.ts +1929 -72
- package/src/extractors/kotlin.ts +5 -5
- package/src/extractors/lua.ts +1 -1
- package/src/extractors/objc.ts +3 -3
- package/src/extractors/ocaml.ts +1 -1
- package/src/extractors/php.ts +2 -2
- package/src/extractors/python.ts +7 -7
- package/src/extractors/ruby.ts +2 -2
- package/src/extractors/scala.ts +1 -1
- package/src/extractors/solidity.ts +1 -1
- package/src/extractors/swift.ts +4 -4
- package/src/extractors/zig.ts +4 -4
- package/src/features/structure-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +494 -20
- package/src/infrastructure/native.ts +87 -5
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +235 -0
- 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
|
+
}
|