Skip to content

bun check: infer from a context sensitive function whose parameter type has an instantiated type argument - #46564

Open
robobun wants to merge 4 commits into
mainfrom
robobun/29a6ccda/check-fixing-mapper
Open

robobun wants to merge 4 commits into
mainfrom
robobun/29a6ccda/check-fixing-mapper

Conversation

@robobun

@robobun robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #46470

Problem

  • bun check reports TS2322: Type 'unknown' is not assignable to type 'boolean' for useSelector((state) => state.isLoading) when the contextual parameter type is State<typeof spreadRow> and spreadRow is a spread of a supabase select result. tsc 7.0.2 infers boolean. The same happens for TanStack Router's useRouterState({ select }).
  • The fixing mapper fixed TSelected with no candidates before the arrow's return type was inferred from. Two causes: instantiate_uncached (src/sema/check/instantiate.rs:1092) instantiated every member of the spread type, so compose put the identity pair TSelected -> TSelected into a property mapper, and params_mentioned_in (src/sema/check/infer.rs:3362) then counted TSelected as mentioned. It also counted every parameter as mentioned when it reached a synthesized type with call signatures.

Fix

  • A type with the symbol of an object literal and no outer type parameters is returned as is by instantiate, as tsgo's getObjectTypeInstantiation does when len(typeParameters) == 0. The Anon, Cond and Substitution branches already return a declared type unchanged under an identity mapper.
  • params_mentioned_in traverses signatures (any_type_in with all = true, the exact mode) instead of marking every parameter. tsgo's InferenceTypeMapper.Map fixes only the parameters that instantiateType reaches, so the set must be exact.
  • Verified: test/cli/check/check.test.ts, "infers the return type of a context sensitive function whose parameter type has an instantiated type argument", four programs (spread, call signature as type argument, instantiated callable interface, depth limit), output verified against tsc 7.0.2. Also all 2406 programs of differential-cases.json against tsc 7.0.2, and the TypeScript conformance sample plus one of eight shards (1646 tests), byte for byte.

Background

  • A mapper is an interned list of (type parameter, type) pairs. An identity pair T -> T is kept on purpose: it records that a declared type depends on T. compose(first, second) adds every pair of second that first does not map, so an identity pair in second spreads into the result.
  • Shape::mapper of a synthesized object type with an object literal symbol maps the literal's outer type parameters to themselves. IDENTITY means there are none, and tsgo does not instantiate such a type at all.
  • fixing_mapper is the mapper tsgo calls context.mapper. Fixing a type parameter freezes its inference, so fixing one the type does not mention loses every candidate found later. Here the arrow's return type was that candidate.
  • Considered skipping identity pairs in any_type_in or in compose: both change what a declared type's mapper means for every reader. Each half on its own fixes only one of the two fixtures.

Downsides

  • params_mentioned_in now walks the signatures of a synthesized type it reaches instead of returning early. On a file with 2000 useSelector calls whose parameter type holds one such type: 1240 ms vs 1265 ms checked (debug build, 3 interleaved runs, noise about 25 ms). On an adversarial file with 1000 calls whose parameter type is an intersection of 800 generic function types: 5.1 s vs 11.7 s (debug build, tsc 7.0.2 takes 0.5 s).
  • Fewer types: on the issue's repro, synthesized types drop from 23 to 20 and any_type_in visits from 1291 to 1289. A literal no longer charges one instantiation per member towards the depth limit, so a false TS2589 near the limit disappears (the deep.ts fixture).
Notes

Self-reviewed: 2 concerns raised, 2 addressed (a fixture for the instantiated callable interface path, and a fixture plus this note for the TS2589 change).

The 8-line reduction without a dependency. infer Rest twice makes Rest count as possibly referenced (more than one declaration), so the type literal in the false branch keeps an identity pair in its mapper. That gives the spread type COULD_CONTAIN_TYPE_VARIABLES, which is what postgrest-js's select parser also produces.

type Parse<S extends string> = S extends `${infer Rest},${infer Rest}` ? never : { type: "field"; name: S };
type Pick1<F extends { name: string }> = { [K in F["name"]]: string };
declare const row: Pick1<Parse<"id">>;
const spreadRow = { ...row };
interface State<TExtra = typeof spreadRow> { isLoading: boolean; extra: TExtra; }
declare function useSelector<TSelected>(selector: (state: State) => TSelected): TSelected;
const isLoading = useSelector((state) => state.isLoading);
export const probe: boolean = isLoading;

The trace of the original: types_assigned_from_contextual_signature -> sig_params -> instantiate_parameter instantiates State<typeof spreadRow> with [TSelected -> TSelected]. The Synth branch composes that into the id property's mapper (a PropSource::Mapped property whose mapper maps to a mapped type, so it is not closed). instantiate_signature_in_inference_context -> fixing_mapper -> params_mentioned_in finds TSelected among the mapper values. get_inferred_type runs with zero candidates and returns unknown. infer_from_signature infers number from the return type afterwards, too late.

Counters (temporary, not in the diff) on the four test programs: any_type_in visits 31 -> 31, synthesized types 25 -> 17.

Other suites run: test/cli/check/check.test.ts in full (the only non-timeout failure, "reports TS2589 for a conditional type that recurs through a type node that isn't deferred", fails the same way on main in this container: it runs out of stack). The timeouts are the 5 s default under describe.concurrent in a debug build.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/check/check.test.ts

…pe has an instantiated type argument

The fixing mapper fixed a type parameter that the contextual parameter
type did not mention, so the return type of the arrow function never
contributed a candidate and the inference was unknown.

Two causes. Instantiating a type with the symbol of an object literal
and no outer type parameters created a new type whose property mappers
carried an identity pair for the type parameter. It is now returned as
is, as getObjectTypeInstantiation does. And params_mentioned_in counted
every type parameter as mentioned when it reached a synthesized type
with call signatures. It now traverses the signatures.
@robobun

robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:55 AM PT - Oct 23rd, 2026

❌ @robobun, your commit 7eea0a9 has 2 failures in Build #131160 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46564

That installs a local version of the PR into your bun-46564 executable, so you can run:

bun-46564 --bun

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 49de575 and 7eea0a9.

📒 Files selected for processing: 3
  • src/sema/check/infer.rs
  • src/sema/check/instantiate.rs
  • test/cli/check/check.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Walkthrough

The checker now marks only inference parameters found during type traversal and returns certain synthesized types unchanged during instantiation. Regression tests cover context-sensitive callback return-type inference with instantiated types and nested instantiations near the depth limit.

Changes

Callback inference

Layer / File(s) Summary
Inference traversal and synthesized types
src/sema/check/infer.rs, src/sema/check/instantiate.rs, test/cli/check/check.test.ts
params_mentioned_in marks only parameters found by traversal. instantiate_uncached returns a synthesized type unchanged under the stated conditions. Regression cases cover spread-derived types, generic signatures, callable interfaces, object-literal members, and nested instantiations. The test asserts expected diagnostics and exit code 1.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 7eea0

No merge-blocking risk is established; the PR is ready for normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly identifies the main change: improving inference for context-sensitive functions with instantiated parameter types. It is specific and related to the changeset.
Description check Passed The description explains the problem, fix, verification results, performance impact, and known test limitations. It does not use the exact template headings, but it provides the required information i…
Linked Issues check Passed Issue #46470 requires bun check to infer boolean for the context-sensitive useSelector callback when the state type uses an object spread in a default type argument. The changes address both rep…
Out of Scope Changes check Passed The changed source files implement the inference fix for issue #46470. The test additions directly verify the reported spread case, the useRouterState impact, related callable-type paths, and the de…
Full details: Description check

Explanation

The description explains the problem, fix, verification results, performance impact, and known test limitations. It does not use the exact template headings, but it provides the required information in equivalent sections.

Full details: Linked Issues check

Explanation

Issue #46470 requires bun check to infer boolean for the context-sensitive useSelector callback when the state type uses an object spread in a default type argument. The changes address both reported inference paths. src/sema/check/instantiate.rs now returns a synthesized object-literal type unchanged when it has no declared outer type parameters. src/sema/check/infer.rs now traverses signatures exactly instead of marking all parameters as mentioned. The regression test in test/cli/check/check.test.ts covers the spread case and the analogous useRouterState({ select }) case. Its snapshots require boolean for both callbacks. The test also covers call-signature and callable-interface paths described by the root cause.

Full details: Out of Scope Changes check

Explanation

The changed source files implement the inference fix for issue #46470. The test additions directly verify the reported spread case, the useRouterState impact, related callable-type paths, and the depth-limit effect of the same instantiation rule. No changed file or test scenario demonstrates an unrelated feature or behavior.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline notes, I also checked the other callers of params_mentioned_in (non_fixing_mapper_comparing, read_type_arguments_for, the conditional-type extends path in mapped.rs, and instantiate_with_outer_return_mapper in call.rs): each only loses over-marking, and any_type_in(_, true, ..) is the same documented exact mode instantiate.rs already relies on. I also confirmed every site that sets Shape::symbol_declared_at (spread, widened literal, rest type) sets shape.mapper from the declaring scope's full identity mapper, so IDENTITY does mean no outer type parameters for the new early return. The repeat helper in check.test.ts is Buffer.alloc-based, so the snapshot normalization follows the test convention.

Extended reasoning...

The change is a two-site fix in the Rust type checker behind bun check (inference fixing mapper and object-type instantiation) plus one multi-fixture test; it touches no security-sensitive surface. The inference semantics are subtle with several callers affected, and the only findings are test-coverage nits, so a human maintainer look is still worthwhile rather than an outright approve.

Comment thread test/cli/check/check.test.ts
Comment thread test/cli/check/check.test.ts
Comment thread src/sema/check/infer.rs Outdated
Comment thread src/sema/check/instantiate.rs Outdated
Comment thread src/sema/check/infer.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the latest push and found no bugs; both points from my earlier review are now covered (member.ts for the object-literal-member selector, over.ts pinning Deep<48> past the limit). Because this changes core inference fixing and object-type instantiation in the checker, a human look is still worthwhile.

What was reviewed:

  • The new Synth early return in src/sema/check/instantiate.rs against every site that sets symbol_declared_at: each also sets Shape::mapper from the declaration scope, so IDENTITY does mean no outer type parameters, matching the documented invariant and tsgo's len(typeParameters) == 0 return.
  • Dropping the may_be_any fallback in params_mentioned_in: the exact all = true traversal is already relied on by derive_instantiation_of_type_parameters, and the only remaining inexact mentions() caller is in print.rs, not an inference site.
  • Worth a human weighing: the PR's own numbers show the adversarial intersection-of-800-generic-functions case going from 5.1 s to 11.7 s (debug), and the Deep<47>/Deep<48> boundary is pinned by snapshot but its agreement with tsc rests on the author's local run.
Extended reasoning...

The diff touches two hunks in the Rust type checker (src/sema/check/infer.rs and src/sema/check/instantiate.rs) plus one new test in test/cli/check/check.test.ts with six fixtures; it touches no security-sensitive surface. The Rust changes are small but sit in inference fixing and object type instantiation, where every caller of params_mentioned_in and every instantiation of an object-literal-symbol'd type changes behavior, and correctness depends on tsc equivalence that cannot be run in this session. The author addressed both prior inline nits in later commits, and no CODEOWNERS entry covers these paths, but the PR's own perf note (an adversarial case roughly doubling in time) is a tradeoff a maintainer should weigh.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant