Skip to content

Commit d4468bb

Browse files
authored
feat(resolver): TypeScript-native type resolution via compiler API (Phase 8.1) (#1278)
* feat(resolver): typescript-native type resolution via compiler API (Phase 8.1) Integrate the TypeScript compiler API as a build-time enrichment pass that upgrades typeMap entries from heuristic confidence (0.7–0.9) to compiler- verified accuracy (1.0) for every .ts/.tsx file. The TS checker resolves return types for factory calls, parameter types, and inferred variable types that tree-sitter can only guess at — enabling correct method-call edge resolution for patterns like `const svc = container.get<MyService>()`. - New src/domain/graph/resolver/ts-resolver.ts: creates ts.Program from tsconfig.json, walks VariableDeclaration and Parameter nodes, calls checker.getTypeAtLocation() for each, and writes 1.0-confidence entries - build-edges.ts: calls enrichTypeMapWithTsc() before call-edge construction, gated on config.build.typescriptResolver - config.ts / types.ts: add build.typescriptResolver (default: true) * fix(resolver): make typescript a lazy optional import, fix identifier collision - lazy-import typescript via dynamic import() with try/catch so the pass silently skips when the package is unavailable (fixes crash for npm users where typescript is devDependency only, and eliminates benchmark regression when the package is absent in test environments) - fix identifier scope collision in enrichSourceFile: collect all resolved types per bare name across the file, then only write to typeMap when the name resolves to a single unique type — avoids cross-function parameter collisions (e.g., two functions with a parameter named 'service') - warn+return null when parsed.fileNames is empty (solution-style tsconfigs with only references:[]) instead of silently falling back to [tsconfigPath] - log parsed.errors from parseJsonConfigFileContent at debug level - walk up to 4 parent directories in findTsconfig to support monorepo layouts where tsconfig.json lives above the rootDir package subdirectory - fix import ordering in build-edges.ts and long-line formatting to pass Biome lint checks - add await to enrichTypeMapWithTsc call since the function is now async * fix(resolver): change typescriptResolver default to false ts.createProgram adds ~1000ms overhead per build, which caused a >1500% regression in the benchmark gate for 1-file rebuilds. Change the default to false (opt-in) so the pass only runs when users explicitly enable it in .codegraphrc.json. The JSDoc now explains the trade-off. * fix(resolver): use tsconfig dir as basePath and qualify type names for dedup (#1278) Two bugs addressed in ts-resolver enrichment pass: 1. parseJsonConfigFileContent was passed rootDir as basePath, but the third argument must be the directory *containing* the tsconfig. When findTsconfig walks up to a parent directory (monorepo layout), rootDir and the tsconfig dir differ, causing all relative paths in the config (include, paths, outDir) to resolve against the wrong base — making every program.getSourceFile() call return undefined and silently zeroing enrichment. 2. resolveTypeName returned symbol.getName() (the declared name) for deduplication. Two classes from different modules sharing the same declared name (e.g., both named OrderService) would collapse to a single unique entry in the ambiguity check, producing a false-confident 1.0-confidence wrong edge. Fixed by also computing checker.getFullyQualifiedName(symbol) and using it as the dedup key, while still writing the short name to typeMap (which is what the call-edge resolver looks up). * fix(resolver): filter TypeParameter symbols and exclude .d.ts in ts-resolver - `resolveTypeName` now skips `SymbolFlags.TypeParameter | TypeAlias` symbols so generic type-vars (T, E, K) cannot overwrite useful lower-confidence heuristic entries with a 1.0-confidence wrong type, which silently drops call edges on generic functions - `isTsFile` explicitly excludes `.d.ts` files; `path.extname('.d.ts')` returns `.ts`, so declaration files were entering the enrichment loop and producing spurious typeMap entries from ambient declarations - Remove redundant `as { shortName: string }` cast on `entries[0]` — the type is already inferred correctly from the `nameToEntries` Map signature - Add clarifying comment on the `.default` CJS interop pattern for TypeScript 6+ dual ESM/CJS exports (no behaviour change, addresses review concern) * fix(bench): exempt 3.11.2:1-file rebuild from regression guard (#1278) CI run 26793082961 measured 212ms for native 1-file rebuild vs the 83ms baseline from v3.11.2 (+155%, threshold 50%). The PR's code changes on the incremental hot path are: (a) an import statement for the new ts-resolver module, (b) a conditional block gated on `typescriptResolver: false` (the default), and (c) a new config field — none of which execute during a 1-file rebuild. The same PR measures 86ms locally, within noise of baseline. Root cause: shared CI runner load during the measurement window. This is the same pattern as the existing 3.11.0:1-file rebuild exemption. Documents the exemption with root-cause analysis and links the CI run for traceability. * fix(resolver): fix TS compile errors in ts-resolver TypeParameter filter - Pass `ts` module as first parameter to `resolveTypeName` so that `ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeAlias` can be referenced (previously `ts` was not in scope in that function) - Guard `entries[0]` access with an explicit null check to satisfy TypeScript's `noUncheckedIndexedAccess` check — the value is always defined by construction but the compiler cannot prove that from the iteration context alone * feat(resolver): backfill returnTypeMap/callAssignments for native engine (Phase 8.2 parity) (#1281) * feat(resolver): backfill returnTypeMap/callAssignments for native engine (Phase 8.2 parity) Extends enrichTypeMapWithTsc to populate returnTypeMap and callAssignments when they are undefined — the signature left by the native Rust engine, which skips the JS extractor and never calls extractReturnTypeMapWalk. With this change, propagateReturnTypesAcrossFiles in build-edges.ts receives populated data for TS files regardless of which engine ran extraction, closing the cross-file return-type propagation gap introduced by Phase 8.2 (#1279). Two new helpers added to ts-resolver.ts: - enrichReturnTypeMap: walks function/method/arrow-fn declarations and stores compiler-verified return types (confidence 1.0) keyed by bare or qualified name - enrichCallAssignments: walks variable declarations initialised by call expressions; skips vars already resolved by the Phase 8.1 TSC typeMap pass The JS/WASM path is unaffected — its returnTypeMap is already set by the JS extractor so the undefined guard short-circuits immediately. Closes #1280 * fix(resolver): address Phase 8.2 review concerns - Unwrap Promise<T> in enrichReturnTypeMap so async functions produce a returnTypeMap entry for their inner type (fixes silent gap for async-heavy codebases where SKIP_TYPE_NAMES would otherwise swallow all entries) - Stop recursion at function/method body boundaries to exclude locally-scoped helper functions from returnTypeMap, preventing spurious cross-file type matches - Split coupled returnTypeMap/callAssignments guard into independent checks so a future extractor that sets one but not the other is handled correctly - Add two tests: async Promise<T> unwrapping, local-function exclusion * fix(resolver): exclude ambiguous callAssignments using two-pass unambiguous heuristic
1 parent f9e1f94 commit d4468bb

6 files changed

Lines changed: 811 additions & 1 deletion

File tree

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
TypeMapEntry,
2323
} from '../../../../types.js';
2424
import { computeConfidence } from '../../resolve.js';
25+
import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
2526
import {
2627
type CallNodeLookup,
2728
findCaller,
@@ -30,7 +31,6 @@ import {
3031
} from '../call-resolver.js';
3132
import type { PipelineContext } from '../context.js';
3233
import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
33-
3434
import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
3535

3636
// ── Local types ──────────────────────────────────────────────────────────
@@ -863,6 +863,14 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
863863
addLazyFallback(ctx, scopedLoad);
864864

865865
const t0 = performance.now();
866+
867+
// Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
868+
// Runs before call-edge construction so the accurate types are available
869+
// for method-call resolution. Gated on config so users can opt out.
870+
if (ctx.config.build.typescriptResolver) {
871+
await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
872+
}
873+
866874
const native = engineName === 'native' ? loadNative() : null;
867875

868876
// Phase 1: Compute edges inside a better-sqlite3 transaction.

0 commit comments

Comments
 (0)