fix(js/ts): seed typeMap from as-casts, resolving the #2235 real-world repro - #2534
Merged
Conversation
…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
Contributor
Greptile SummaryThe PR teaches both JavaScript/TypeScript extraction engines to seed scoped type-map entries from the final target of an
Confidence Score: 5/5The 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
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
Reviews (1): Last reviewed commit: "fix(js/ts): seed typeMap from as-casts, ..." | Re-trigger Greptile |
Contributor
Codegraph Impact Analysis2 functions changed → 9 callers affected across 1 files
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:is an
as-cast, not a type annotation or a barenew X(). Neither engine'shandleVarDeclaratorTypeMap/handle_var_declarator_type_maphad a branch foras_expressionvalues, so this line contributed nothing to the typeMap in either engine. With no scoped entry, resolution fordb.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 Yis handled by extracting from the outermostas_expression's own type child — this naturally yields the finalYwithout needing to special-case the intermediateunknownhop.handle_var_declarator_type_map's ordering:dedup_type_mapis 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 diffedopenReadonlyOrFail's edges directly from the resulting graph.db — byte-for-byte identical on kind, target, confidence, and technique, including thereceiveredge (nowBetterSqlite3Databaseon both, wasLockedDatabaseon native) and the previously-missingdb.prepare/db.pragmacall edges (bothBetterSqlite3Database.*at 0.6ts-nativeandNativeDbProxy.*at 0.8cha, matching exactly).Test plan
tests/parsers/javascript.test.ts(basic cast, chainedas unknown as X, bareas unknownseeds nothing, cast wins over annotation, doesn't mistake the cast's identifier input for the target type).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.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 warningsandcargo fmt --checkclean.npm test: 5332 passed.npm run lintclean.codegraph diff-impact --staged -T: bounded toextractAsExpressionTypeName/handleVarDeclaratorTypeMapand their existing callers within the same file.