Skip to content

fix(js/ts): seed typeMap from as-casts, resolving the #2235 real-world repro - #2534

Merged
carlos-alm merged 1 commit into
mainfrom
fix/issue-2397
Aug 16, 2026
Merged

fix(js/ts): seed typeMap from as-casts, resolving the #2235 real-world repro#2534
carlos-alm merged 1 commit into
mainfrom
fix/issue-2397

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Closes #2397

Context

#2235 fixed a general typeMap-scoping-collision bug and was verified correct and complete for that general case. But the specific real-world repro that originally motivated #2235 — this repo's own src/db/connection.ts — was still divergent between engines after that fix landed, as this issue documents.

Root cause

openReadonlyOrFail's own local:

const db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;

is an as-cast, not a type annotation or a bare new X(). Neither engine's handleVarDeclaratorTypeMap/handle_var_declarator_type_map had a branch for as_expression values, so this line contributed nothing to the typeMap in either engine. With no scoped entry, resolution for db.pragma()/db.prepare() fell through to the bare "db" key, whose winner depended on confidence/insertion-order in the return-type-propagation branch (Phase 8.2) — luck that happened to differ between engines (WASM resolved correctly via that path; native didn't).

Fix

Rather than chasing the fragile propagation-order divergence (the issue's suggested "fix #1"), this addresses the issue's suggested "fix #2": seed the typeMap directly from the as-cast's target type — the cast is what the rest of the file actually treats the value as, at confidence 0.9 (same tier as an explicit type annotation), with the same priority as the existing constructor branch (an explicit initializer shape wins over a declared annotation on the same statement).

  • X as unknown as Y is handled by extracting from the outermost as_expression's own type child — this naturally yields the final Y without needing to special-case the intermediate unknown hop.
  • The Rust side required restructuring handle_var_declarator_type_map's ordering: dedup_type_map is first-write-wins on confidence ties, and the cast is pushed at the same 0.9 tier as a type annotation — so simply pushing both and relying on confidence comparison (as the constructor branch's unambiguous 1.0-vs-0.9 gap already could) would let the annotation win the tie instead of the cast. The cast/constructor checks now run first and skip the annotation push when either already seeded a more authoritative entry.

Verification against the actual reported repro

Rebuilt this repo's own src/ with both engines end-to-end and diffed openReadonlyOrFail's edges directly from the resulting graph.db — byte-for-byte identical on kind, target, confidence, and technique, including the receiver edge (now BetterSqlite3Database on both, was LockedDatabase on native) and the previously-missing db.prepare/db.pragma call edges (both BetterSqlite3Database.* at 0.6 ts-native and NativeDbProxy.* at 0.8 cha, matching exactly).

Test plan

  • TS: 5 new tests in tests/parsers/javascript.test.ts (basic cast, chained as unknown as X, bare as unknown seeds nothing, cast wins over annotation, doesn't mistake the cast's identifier input for the target type).
  • Rust: 5 matching new tests in crates/codegraph-core/src/extractors/javascript.rs, plus 3 verified to fail without the fix (revert-rebuild-confirm-restore cycle) — including one that specifically proves the tie-breaking restructure (as_cast_wins_over_a_same_declaration_type_annotation), not just the basic seeding.
  • 2 new cross-engine parity tests in tests/engines/parity.test.ts, verified to fail against a native addon built from the pre-fix source.
  • cargo test: 1045 passed. cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.
  • npm test: 5332 passed. npm run lint clean.
  • codegraph diff-impact --staged -T: bounded to extractAsExpressionTypeName/handleVarDeclaratorTypeMap and their existing callers within the same file.

…d repro (#2397)

#2235's scoping fix was correct and complete for the general collision case,
but the specific real-world repro that motivated it — this repo's own
src/db/connection.ts — was still divergent: openReadonlyOrFail's own local
(`const db = new Database(...) as unknown as BetterSqlite3Database`) is an
as-cast, not a type annotation or bare `new X()`, so neither engine's
handleVarDeclaratorTypeMap had a branch for it. With no scoped entry, `db`'s
resolution fell through to the bare "db" key, whose winner depended on
confidence/insertion-order luck in the return-type-propagation branch — luck
that differed between engines (wasm happened to resolve correctly via that
path, native didn't).

Rather than chasing that fragile propagation-order divergence, this seeds the
typeMap directly from the as-cast's target type — the cast is what the rest
of the file actually treats the value as — at confidence 0.9 (the same tier
as an explicit type annotation), checked with the same priority as the
existing constructor branch (an explicit initializer shape wins over a
declared annotation). `X as unknown as Y` is handled by extracting from the
OUTERMOST as_expression's own type child, which naturally yields the final Y
without special-casing the intermediate unknown hop.

The Rust side required restructuring handle_var_declarator_type_map's
ordering: dedup_type_map is first-write-wins on confidence TIES, and the cast
is pushed at the same 0.9 tier as a type annotation, so simply pushing both
and relying on confidence comparison (as the constructor branch's
unambiguous 1.0-vs-0.9 gap already could) would let the annotation win the
tie instead of the cast. The cast/constructor checks now run first and skip
the annotation push when either already seeded a more authoritative entry.

Verified against the actual reported repro end-to-end: rebuilding this
repo's own src/ with both engines now produces byte-for-byte identical edges
(kind, target, confidence, technique) for openReadonlyOrFail, including the
receiver edge and the previously-missing db.prepare/db.pragma call edges.

docs check acknowledged

Impact: 2 functions changed, 0 affected
@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR teaches both JavaScript/TypeScript extraction engines to seed scoped type-map entries from the final target of an as cast.

  • Adds mirrored TypeScript and Rust cast-target extraction.
  • Gives cast initializers precedence over same-declaration annotations.
  • Adds parser-level and cross-engine parity coverage for simple, chained, unknown-only, and conflicting casts.

Confidence Score: 5/5

The PR appears safe to merge; no actionable correctness, security, or parity defects were identified.

The native and TypeScript implementations apply equivalent cast-target extraction and precedence rules, with focused parser and cross-engine tests covering the changed behavior.

Important Files Changed

Filename Overview
src/extractors/javascript.ts Adds cast-target extraction and scoped type-map seeding while preserving existing initializer precedence.
crates/codegraph-core/src/extractors/javascript.rs Mirrors the TypeScript behavior in the native extractor and explicitly handles equal-confidence deduplication.
tests/parsers/javascript.test.ts Covers basic and chained casts, unknown-only casts, annotation precedence, and identifier-input disambiguation.
tests/engines/parity.test.ts Verifies native and WASM engines produce matching cast-derived type-map entries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Variable declarator] --> B{Initializer kind}
  B -->|new expression| C[Seed constructor type at 1.0]
  B -->|as expression| D[Extract outermost cast target]
  D -->|Nameable target| E[Seed scoped type at 0.9]
  D -->|unknown or unsupported target| F[Try declared annotation]
  B -->|Other initializer| F
  F --> G[Seed annotation at 0.9 when present]
  C --> H[Type map used for receiver resolution]
  E --> H
  G --> H
Loading

Reviews (1): Last reviewed commit: "fix(js/ts): seed typeMap from as-casts, ..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

2 functions changed9 callers affected across 1 files

  • extractAsExpressionTypeName in src/extractors/javascript.ts:2749 (3 transitive callers)
  • handleVarDeclaratorTypeMap in src/extractors/javascript.ts:3683 (13 transitive callers)

@carlos-alm
carlos-alm merged commit 384f90a into main Aug 16, 2026
34 checks passed
@carlos-alm
carlos-alm deleted the fix/issue-2397 branch August 16, 2026 04:38
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Residual native/wasm divergence on db/connection.ts survives #2235's typeMap-scoping fix

1 participant