Integration test: 2026-08-04 - #49
Draft
apiology wants to merge 376 commits into
Draft
Conversation
Confirmed by running the same repros against a branch with castwide#1266 already merged: - the two-conjunct Hash#fetch dispatch specs are blocked on castwide#1266 for the generic<X> leak, but will still fail afterward on a separate, unfixed first-conjunct-only dispatch bug - the three method-call-on-intersection-receiver specs reproduce identically with or without castwide#1266 - unrelated code path (Chain::Call#resolve, not Pin::Parameter#compatible_arg?) So merging castwide#1266 will not silently flip any of these to passing; each still needs its own dispatch/resolution fix. Still 66 examples, 0 failures, 8 pending; rubocop clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZme4n9mb8hGU8mrw94NAV
…1231) Chain::Call#resolve applied union call-semantics (every alternative must define the method, unless loose_unions) to every unique type produced by binder.each_unique_type - and that flattens straight through an Intersection conjunct-by-conjunct, so `A & B#foo` (foo on A only) required foo on *both* A and B and came back unresolved. Split the walk into two levels: method_pins_for_binder applies the existing strict union semantics across a ComplexType top-level (each alternative must resolve), while method_stack_pins handles a single unique type and gives Intersection conjuncts the opposite, correct rule - any one conjunct defining the method is enough (A & B <: A, A & B <: B) - recursing per conjunct since RBS allows a union inside an intersection member, e.g. (A | B) & C. Flips the 3 method-resolution specs added earlier from pending to passing; the 3 Hash#fetch dispatch specs (blocked on castwide#1266 and/or the separate first-conjunct-only bug) are untouched by this, as expected - this fix does not touch compatible_arg? or per-conjunct #fetch dispatch at all. Verified: full suite 1686 examples, 1 pre-existing unrelated failure (spec/pin/method_spec.rb:516, reproduces identically on unmodified HEAD), 0 regressions; rubocop clean (pre-existing offenses at line 170 untouched); self-typecheck --level strong on call.rb clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZme4n9mb8hGU8mrw94NAV
Found while verifying the intersection method-call-resolution fix (342b11b) did not regress real union semantics: loose_unions: false should deny a call when only one member of a plain two-class union defines it, but does not. The existing strict-mode spec only covers this rule via nil-stripping (nullable?/without_nil), never the general two-real-class case. Confirmed pre-existing - reproduces identically on unmodified HEAD, before 342b11b. Filed as its own GitHub issue for discussion: castwide#1270 covers a different, unrelated bug found along the way (Chain#nullable? nil leak); this union bug is tracked via task #2 for a pre-merge discussion, not yet filed separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZme4n9mb8hGU8mrw94NAV
…calls Chain#nullable? checked whether *any* link in the chain used safe navigation (&.), so a chain like `x&.to_s == '1'` had nil appended to the inferred type of the trailing `==` call even though `==` always returns Boolean. Check only the chain's last link instead, since that's the link whose result the chain actually evaluates to. Fixes castwide#1270 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRSe8diudqUUud4dNkV6MD
A plain union of two Hash instantiations (Hash{...}, Hash{...}, no
& at all) shows the identical always-first-member dispatch bug as
the same-class intersection specs already here. Verified with a
minimal @Generic Box class (no Hash, no literal keys, no castwide#1266) that
this also reproduces byte-identically on unmodified
castwide/solargraph master (8fda633) - confirms the root cause is
Call#inferred_pins binding a class generic against the whole
union/intersection self_type instead of per-member, unrelated to
anything castwide#1231 or castwide#1266 introduced.
Intent: fix this as its own PR against master so the Hash
intersection specs inherit it regardless of merge order, rather
than stacking this branch on top of a dependency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZme4n9mb8hGU8mrw94NAV
…ail link
Only checking the chain's last link (as the previous commit did) is
unsound: a call between the earlier &. and the end of the chain can
still return nil if it's a self-returning method (e.g. #tap,
#itself), since &. only skips the immediate call - a later
non-safe-navigated call still runs on whatever that produced. Verified
this false negative empirically (x&.to_s.itself / x&.to_s.tap{} were
silently un-flagged after the previous commit's fix).
nullable? now walks the chain tracking whether nil could still be
flowing: a &. link sets it, and a later non-&. call link clears it
unless NilClass's own definition of that method name returns self
(determined by checking the raw declared return type on NilClass,
before self-substitution) - matching how Ruby actually evaluates a
non-nil-safe call made on a nil receiver.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRSe8diudqUUud4dNkV6MD
Owner
Author
|
Closing/reopening to force-retrigger CI ��� the push to head 73d3333 never dispatched the RSpec/Typecheck/Plugin/Linting workflows (no run object exists for this SHA), while CodeQL did fire. This looks like a missed webhook delivery, not a code issue. ��� Claude |
When a receivers declared type unions multiple instantiations of the same generic class (e.g. Box<Integer>, Box<String>), Chain::Call#resolve looked up a method pin per union member, then deduped the results by path alone. Since both members resolve to the same method path (Box#get) but had already been resolved to different, correct return types for their own context, the dedup silently discarded every member but the first - so the inferred type depended on declaration order instead of being a real union. Fixes castwide#1272 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoPQ2EZUCHYMwDr13PBsc4
always_leaves_compound_statement? only recognized a raise/fail call when it was the clause's sole statement (a :send node). A branch with more than one statement -- e.g. building an error message before raising -- parses as a :begin node, which fell through to the :send check and returned false, so the guard never narrowed the checked variable for the rest of the method. Move the check into ParserGem::NodeMethods so it can be shared with the chain-inference fix in the next commit, and recurse into a :begin clause's last child before applying the raise/fail shape check. Addresses a PR review comment on castwide#1259.
Chain::Or#resolve inferred the result of an 'or' expression as the
union of both sides' types. When the right-hand side is a call that
never returns control (raise/fail), its inferred type is 'undefined'
-- and ComplexType collapses any union containing an undefined item
down to just 'undefined', so argv[0] || raise('...') inferred as
undefined instead of argv[0]'s non-nil element type, and TypeChecker
reported the enclosing method's return type as uninferrable.
Since a raise/fail rhs never contributes a value, reaching code past
the 'or' expression implies the lhs was truthy, so the result type is
just the lhs type with nil excluded.
Addresses a PR review comment on castwide#1259.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NF1ZWsAo2LdLTQP1jvbmFi
Applied the same fix as castwide#1273 (order-dependent generic resolution for same-class union receivers) to Call#method_stack_pins Intersection branch: both conjunct dedup points now key on [path, return_type.tag] instead of path alone, so a same-class intersection (e.g. Hash{K1=>V1} & Hash{K2=>V2}) no longer silently drops every conjunct but the first. This makes Hash#fetch dispatch order-independent and sound (returns the union of every conjunct plausible result), but not yet precise - true per-key narrowing needs the literal Hash key ("Index" vs "Triggers") to survive Pin::Parameter#typify, and UniqueType#qualify unconditionally widens literal types to their base class. Attempted gating that on a corrected #literal? check (the existing one is unconditionally disabled by castwide#1201, for an unrelated array/tuple-inference reason) but reverted it: the same code path is load-bearing for other tested behavior (RBS `NilClass#to_s: () -> ""` widening to String, true/false -> Boolean consolidation), which broke under the naive fix (spec/rbs_map/core_map_spec.rb:102,114 and spec/parser/flow_sensitive_typing_spec.rb:644). A real fix needs qualify/transform to distinguish a key_types position from a general return-type position, which is a larger change than this commit attempts. Updated the two affected pending specs to describe the current, accurate remaining gap (union-not-precise-narrowing + castwide#1266) instead of the now-fixed order-dependence. Verified: full suite 1688 examples, 1 pre-existing unrelated failure, 0 regressions; rubocop clean (pre-existing offenses untouched). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZme4n9mb8hGU8mrw94NAV
…only inference Pulls in two new upstream commits on top of the already-merged castwide#1259 base: - Recognize multi-statement raise/fail branches in flow-sensitive typing: always_leaves_compound_statement? now recurses into :begin nodes' last child, so a clause like `msg = "bad"; raise msg` is still recognized as unconditionally leaving, not just a single bare `raise`/`fail` send. - Infer only the lhs type for `x || raise(...)` and `x ||= raise(...)`: new logic in node_chainer.rb and Chain::Or#resolve treats a never-returning rhs the same way as the flow-sensitive-typing narrowing already does, so the combined type is just the lhs's non-nil type instead of a union with the (unreachable) rhs. Conflict in lib/solargraph/parser/flow_sensitive_typing.rb: the upstream commits move always_leaves_compound_statement? out of FlowSensitiveTyping and into the shared Solargraph::Parser::ParserGem::NodeMethods module (aliased as Solargraph::Parser::NodeMethods, already included by FlowSensitiveTyping), so it can be reused from node_chainer.rb, and extend it with :begin-node support. Removed the now-duplicate local copy in favor of the shared one; kept this branch's own :closure attr_reader addition (from #53, needed by every other FlowSensitiveTyping.new caller already updated on this branch). Also dropped two now-superfluous `@sg-ignore Need to add nil check here` comments in node_chainer.rb (above the always_leaves_compound_statement?(or_asgn_rhs_node) and always_leaves_compound_statement?(or_rhs_node) calls): on the PR's own source branch, NodeChainer.chain's node arguments are inferred as `Array, nil` throughout that branch (a broad, unrelated pre-existing mismatch visible across dozens of lines when typechecked standalone), so the @sg-ignore was suppressing a real mismatch there. On this integration branch those same node variables are already correctly typed `Parser::AST::Node, nil` (fixed by an earlier-merged PR), and always_leaves_compound_statement?'s own param is already declared nilable, so passing them needs no suppression - the local Solargraph typecheck hook correctly flagged both comments as unneeded. Verified: spec/parser/flow_sensitive_typing_spec.rb, spec/parser/node_methods_spec.rb, spec/source/chain/or_spec.rb, spec/parser/node_chainer_spec.rb (134 examples, 0 failures, 4 pending), and a broader safety net — spec/parser, spec/source, spec/source_map/clip_spec.rb (475 examples, 0 failures, 14 pending) — all passing locally.
Pulls in 8 new upstream commits: a fix for method-call resolution on intersection-typed receivers (an Intersection conjunct only needs one conjunct to define the method, unlike a union where every alternative must), a fix for order-dependent Hash intersection dispatch, and several pending-spec/documentation commits (including two that document the Hash#fetch generic leak already fixed by castwide#1266 on this branch). Conflict in lib/solargraph/source/chain/call.rb, in two parts: - Chain::Call#resolve's inline union-only pin lookup (each_unique_type + get_method_stack) is replaced by the incoming branch's method_pins_for_binder, which generalizes it to also handle intersections (via a new private method_stack_pins helper) - took the incoming version entirely, since it's a strict superset. - The private-methods section had HEAD's match_overload_type (castwide#1247) and narrowed_call_pin (castwide#1258) on one side and the incoming method_pins_for_binder/method_stack_pins pair on the other; all four are independent and still called from unconflicted parts of the file, so kept all four as sibling private methods. Also dropped a `pending 'blocked on castwide#1266 ...'` marker on spec/type_checker/levels/strong_spec.rb's Hash#fetch generic-leak test: castwide#1266 (structural RBS interface-typed expectation checks), already merged into this branch, fixes exactly what the test's own comment predicted - confirmed via "Expected pending ... to fail. No error was raised." Investigated an apparent regression in spec/source_map/clip_spec.rb (11 tuple-related failures, all returning "undefined") surfaced by the post-merge broader safety-net run: traced it to ComplexType#qualify failing to resolve Solargraph::Fills::Tuple via api_map.qualify, root caused to a stale local PinCache disk cache left over from earlier in this session (PinCache.work_dir keys off Solargraph::VERSION's branch-derived dev string, which doesn't change within a branch, so a cache built before this merge can persist and mask/corrupt later results). Clearing ~/.cache/solargraph/ruby-3.2.6/rbs-4.1.2/solargraph-* made all 11 failures disappear - confirmed not a real regression by diffing behavior against a clean detached checkout of the pre-merge commit with the same (then also cleared) cache. Verified: spec/source/chain/call_spec.rb, spec/type_checker/levels/strong_spec.rb, spec/complex_type/conforms_to_spec.rb (159 examples, 0 failures, 10 pending), and a broader safety net - spec/type_checker, spec/source, spec/source_map/clip_spec.rb, spec/complex_type_spec.rb (799 examples, 0 failures, 33 pending) - all passing locally with a clean cache.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoPQ2EZUCHYMwDr13PBsc4
Pin::Callable#arity_matches? rejected any call missing a block whenever the method signature had block info attached, even when that info came from a bare &block formal parameter with no @yield tags. Ruby never requires callers to pass a block for such a parameter, so this caused the sole matching signature to be discarded, skipping generic resolution and leaving the return type as unresolved generic<T>. Add Pin::Callable#block_required?, true only for RBS-sourced signatures with a non-optional block ({ ... } vs ?{ ... }), and gate the arity check on it instead of bare block presence. YARD-derived signatures have no way to express a required block, so they default to false. Fixes castwide#1265 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNnFsUN4Uryo6Xqh7xv2sr
Adds Pin::Callable#block_required? (default false), true only for
RBS-sourced signatures with a non-optional block (`{ ... }`, not
`?{ ... }`). Pin::Callable#arity_matches? previously rejected a call
missing a block whenever the signature declared *any* block, even an
optional one - so a bare `&block`/@yield-tag signature with a @Generic
return type would get skipped by overload resolution for a callsite
with no block, falling through to a less specific overload and losing
the generic. Now it only rejects when block_required? is true.
Conflict in lib/solargraph/rbs_map/conversions.rb: HEAD didn't know
about the new block_required: keyword yet; took the incoming side,
which wires overload.method_type.block&.required into the new
Pin::Signature parameter (mirroring the equivalent, non-conflicting
change already applied to rbs_translator.rb#to_signature).
Verified: spec/source/chain/call_spec.rb, spec/rbs_map/conversions_spec.rb,
spec/rbs_translator_spec.rb (59 examples, 0 failures, 3 pending), and a
broader safety net - spec/type_checker, spec/source,
spec/source_map/clip_spec.rb, spec/pin (792 examples, 0 failures, 25
pending) - all passing locally.
…arameters castwide#1228 fixes the same underlying bug as the already-merged castwide#1266 (issue castwide#1227: RBS 4.1's Hash#fetch takes its key as the Hash::_Key duck-type interface instead of a generic, causing Solargraph to fall back to the unresolved generic<X> from the block-form overload) but via a different, earlier mechanism: a blanket :allow_unmatched_interface bypass in Pin::Parameter#compatible_arg?, rather than castwide#1266's later structural Conformance check. Verified castwide#1228's own regression test already passes unmodified on this branch without its compatible_arg? change (isolated it into a standalone spec file and ran it against HEAD before resolving the conflict) - castwide#1266's structural interface verification already covers this case, making castwide#1228's code change redundant here. Kept HEAD's compatible_arg? as-is (including literal_arg_matches?, from an earlier-merged PR that castwide#1228's branch, based directly on castwide/master, never saw) and dropped castwide#1228's interface-bypass hunk entirely. Conflict in spec/type_checker/levels/strong_spec.rb: kept castwide#1228's new regression test (issue castwide#1227) as a sibling of HEAD's intersection-type test block (from castwide#1231), which castwide#1228's branch also never saw. .github/workflows/rspec.yml auto-merged cleanly, taking castwide#1228's RBS matrix bump (4.0.0/4.0.1/4.0.2 -> 3.10.0/4.0.3/4.1.1) - core to what this PR is actually testing (RBS 4.1's Hash#fetch signature change). Verified: spec/type_checker/levels/strong_spec.rb, spec/pin/parameter_spec.rb (104 examples, 0 failures, 5 pending), and a broader safety net - spec/type_checker, spec/source, spec/source_map/clip_spec.rb, spec/complex_type, spec/complex_type_spec.rb (807 examples, 0 failures, 35 pending) - all passing locally.
#49 CI caught a real gap left over from an earlier merge on this branch: I dropped this test's `pending` marker while merging the latest castwide#1231 commits, having confirmed locally (RBS 4.1.2) that castwide#1266 fixes the leak - but only verified against that one RBS version. CI's full matrix showed `rspec (4.0, 3.10.0)` still failing with the exact leak (`Declared type Float does not match inferred type Float, generic<X>`), while `rspec (4.0, 4.1.1)` passes; every other leg was a fail-fast cancellation of the one real failure, not an independent failure (confirmed via `gh api .../jobs/<id> --jq '.conclusion'` per job). So castwide#1266 fixes this only for RBS >= 4.1.0, matching the same cutover already tracked in spec/rbs_map/conversions_spec.rb and spec/convention/activesupport_concern_spec.rb. A bare `pending` would have been wrong in the other direction - it would break CI's RBS 4.1.x legs, which currently pass this test with no pending marker. Made the assertion itself branch on `Gem::Version.new(RBS::VERSION)` instead, so the test actively verifies the correct behavior for whichever RBS version each matrix leg runs, rather than skipping any of them. Verified: spec/type_checker/levels/strong_spec.rb (74 examples, 0 failures, 5 pending) against local RBS 4.1.2, and a broader safety net - spec/type_checker, spec/complex_type_spec.rb, spec/complex_type (465 examples, 0 failures, 24 pending).
raise/fail/abort are declared in RBS as returning `bot`, meaning the expression never actually produces a value and is therefore compatible with any expected type. RbsTranslator collapsed Bottom into the same 'undefined' tag used for Any, so raise-only method bodies failed typecheck with "return type could not be inferred" instead of being compared against the declared @return tag, and bot values leaking into generic resolution (e.g. Array#fetch's block form were never recognized as auto-compatible with the expected type. Fixes castwide#1276 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ncz9vtnjnpYpotyVRG4Eq EOF )
Splits RBS::Types::Bases::Bottom out of the combined `Any, Bottom -> 'undefined'` case in RbsTranslator#type_to_tag, giving it its own 'bot' tag instead. Wires the new bot? predicate through ComplexType#qualify, UniqueType#qualify, and UniqueType#conforms_to? (bot is a subtype of every type, so it short-circuits conformance checks the same way :allow_undefined does, but as a type-theoretic fact rather than a leniency rule) and through TypeChecker#method_return_type_problems_for (a method body that only ever raises/aborts is compatible with any declared return type). Conflict in lib/solargraph/rbs_translator.rb: incoming's branch, based directly on castwide/master, still had the old `ClassInstance, Alias, Interface` / `ClassSingleton` cases in type_to_tag that were already deliberately removed on this branch during an earlier merge (they called an undefined type_tag method - see the castwide#1231 merge commit). Kept this branch's structure, just narrowed the `Any, Bottom` case down to `Any` alone - the new `Bottom -> 'bot'` case (with updated comment) was already present via the merge's own unconflicted auto-merge, immediately following. Verified: spec/rbs_translator_spec.rb, spec/type_checker/levels/typed_spec.rb, spec/complex_type_spec.rb, spec/complex_type (214 examples, 0 failures, 12 pending), and a broader safety net - spec/type_checker, spec/source, spec/source_map/clip_spec.rb (644 examples, 0 failures, 23 pending, after clearing a stale local PinCache disk cache that caused unrelated tuple-spec failures the same way it did during the castwide#1231 latest-commits merge earlier in this session) - all passing locally.
pp's `class << ENV ... end` (to add pretty_print) makes YARD parse ENV as a Class, contradicting RBS's correct ENVClass instance type for the same path. Solargraph kept both pins and unioned their types at lookup time, so strong-level method resolution required ENV to satisfy both types simultaneously - breaking ENV.fetch, ENV[], and ENV[]= wherever pp is loaded (i.e. almost everywhere, since pp ships in stdlib as of Ruby 3.x). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPVNYNrvUNJwdH7UyAYM8A
Call#method_stack_pins's Intersection branch returned a union of every
conjunct's return type for calls like Hash{"Index" => Float}
& Hash{"Triggers" => Array<...>}#fetch("Index"), instead of narrowing
to the one conjunct whose key actually matches. RBS's own
Hash#fetch: (_Key key) -> V can't do this itself - _Key is a
structural hash/eql? interface, not literally K, so the key argument
is never connected to the return type by ordinary overload
resolution.
This detects any _Key-shaped parameter on a conjunct's method
(generalizing past #fetch/#[] to #dig, #delete, etc. without naming
them) and, only when every conjunct yields a positive verdict for or
against the call's own literal argument, keeps just the matching
conjunct(s) - falling back to today's full union whenever even one
conjunct can't be verified one way or the other, so nothing is ever
narrowed away without positive evidence.
Both specs demonstrating this are still pending on this branch: they
also need castwide#1223 (literal type inference, so the
literal key_types survive to be compared at all) and, on RBS >= 4.1.x,
castwide#1266 (structural RBS interface conformance, so
Hash#fetch's own overload resolution doesn't leak generic<X>).
Neither is specific to this fix or to intersections - verified this
branch alone already loses literal keys before castwide#1223, and is clean on
RBS 3.10.x but leaks generic<X> on RBS >= 4.1.x without castwide#1266.
`pp`'s `class << ENV ... end` (added to define `pretty_print`) makes YARD parse ENV as a Class, contradicting RBS's correct ENVClass instance type for the same path. Solargraph kept both pins and unioned their types at lookup time, breaking ENV.fetch/ENV[]/ENV[]= wherever pp is loaded (i.e. almost everywhere, since pp ships in stdlib as of Ruby 3.x). Adds GemPins::KNOWN_BAD_YARD_PINS to drop the misdeclared YARD pin before it enters the pin set. Conflict in lib/solargraph/gem_pins.rb: incoming's branch was written against the pre-castwide/solargraph#1252 architecture, where GemPins.build_yard_pins was the real call site for building a gem's YARD pins. That method was superseded by Yardoc.build_pins during castwide#1252's PinCache rewrite, already on this branch - reintroducing build_yard_pins wholesale would have left it dead code with no caller. Kept the KNOWN_BAD_YARD_PINS table in gem_pins.rb but wired the actual filtering into Yardoc.build_pins (yardoc.rb), the pin-building call site this branch currently uses. Verified: spec/gem_pins_spec.rb, spec/yardoc_spec.rb, spec/doc_map_spec.rb (27 examples, 0 failures, including the new ENV regression test), and a broader safety net - spec/type_checker, spec/source, spec/source_map/clip_spec.rb, spec/api_map_spec.rb, spec/api_map_method_spec.rb (727 examples, 1 failure, 27 pending). The 1 failure (spec/api_map_spec.rb:771, "resolves aliases for YARD methods") is a pre-existing order-dependent flake unrelated to this merge - confirmed by reproducing the identical single failure running the same spec set against a throwaway worktree at pre-merge HEAD.
CI's full matrix caught a pre-existing gap unrelated to this branch's Hash-intersection work: this spec was unconditionally pending, but castwide#1266 (which fixes the leak) isn't merged into this branch, so the leak was assumed to reproduce on every RBS version. CI's "rspec (3.1, 3.10.0)" leg unexpectedly passed it (an RSpec "pending example fixed" failure), cascading a fail-fast cancellation across the rest of the matrix. Mirrors the same RBS-version-aware pattern already applied to this same spec on branch 2026-08-04 (which does have castwide#1266) in commit ac4eb27 - just inverted, since without castwide#1266 here the leak only reproduces on RBS >= 4.1.0, not below it.
…-shaped literal key match Adds Call#key_verified_conjuncts (and its helpers conjunct_key_verdict/unique_type_key_verdict/literal_node_tag) to Call#method_stack_pins's Intersection branch: when every conjunct of a same-class Hash intersection yields a positive verdict for or against the call's own literal argument at a `_Key`-shaped parameter (RBS's `Hash#fetch: (Hash::_Key key) -> V` and friends), narrows to just the matching conjunct(s) instead of returning a union of every conjunct's result. Conservative by construction - falls back to the full unfiltered union whenever even one conjunct can't be verified. Adds UniqueType#literal_keyed?/#key_type_tag? and Signature#key_param_index as supporting primitives. Conflict in spec/type_checker/levels/strong_spec.rb: both sides independently touched the same 'leaks an unresolved generic<X> from Hash#fetch' spec's comment/pending logic - kept this branch's version (this branch has castwide#1266 merged, so the leak only reproduces below RBS 4.1.0; incoming's branch lacks castwide#1266, so its version of the same spec was inverted). Also dropped two now-stale `pending` markers on 'dispatches generic methods per-conjunct when intersecting two instantiations of the same generic class (castwide#1231)' and 'dispatches generic methods per-conjunct regardless of conjunct order (castwide#1231)' - both were pending on castwide#1223 and, on RBS >= 4.1.x, castwide#1266, both of which are already merged into this branch, so the new _Key-narrowing fix makes them pass outright here. Also rewrote Call#key_verified_conjuncts's `conjuncts.zip(verdicts).select { |(_c, matched)| matched }.map(&:first)` as a plain imperative each_with_index/push loop - this repo's own pre-commit self-typecheck hook (bundle exec solargraph typecheck --level strong, full project context) couldn't soundly infer the chained Enumerable form's return type through three different rewrites (zip+destructured select resolved to Kernel#select instead of Array#select; select.with_index hit an unresolved Enumerator#with_index; each_index.select.map inferred a nonsensical Array<ComplexType>, Array<Array<ComplexType>, nil> return type). The imperative form typechecks cleanly project-wide and is behaviorally identical. Verified: spec/type_checker/levels/strong_spec.rb, spec/source/chain/call_spec.rb, spec/complex_type_spec.rb, spec/complex_type (277 examples, 0 failures, 18 pending), and a broader safety net - spec/type_checker, spec/source, spec/source_map/clip_spec.rb, spec/api_map_spec.rb, spec/api_map_method_spec.rb, spec/pin (879 examples, 1 failure, 27 pending). The 1 failure (spec/api_map_spec.rb:771, "resolves aliases for YARD methods") is the same pre-existing order-dependent flake already confirmed unrelated to this branch's work during the castwide#1278 merge earlier in this session.
Gem::Specification.find_by_path already resolves relative to a gem's
own require_paths, so passing "lib/#{require}.rb" instead of the bare
require path always missed. This silently dropped any gem whose
conventional require path differs from its RubyGems package name
(e.g. activesupport/active_support), along with its transitive
dependencies, with no error or warning.
Reported in castwide#1252 (comment)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yvznkiHNc5iXyycEj8tmR
Porting PR 1311's fallback_pins mechanism from the pre-1252 DocMap#deserialize_combined_pin_cache onto this branch's PinCache#deserialize_combined_pin_cache, which 1252 rewrote without carrying it forward. Known issue: since deserialize_combined_pin_cache now returns non-nil via the fallback, the caller's uncached_gemspecs queue never gets populated for a gem that hits this path, so no real combined cache ever gets built for it. Still being investigated.
deserialize_combined_pin_cache can now return RbsMap#fallback_pins before the real combined cache exists (see the prior commit). This call site inferred "is this gem cached" from "did I get pins back", which was true before that fallback existed but stopped being true once a fallback answer could also be non-nil. Left as-is, a gem that hits the fallback path would never get queued into uncached_gemspecs, so no real combined cache (with YARD pins merged in) would ever get built for it. Check pin_cache.cached? separately instead, only when pins came back at all - when nothing came back, it's unconditionally uncached already and a second check is redundant.
…ns port and real-cache queueing fix # Conflicts: # lib/solargraph/doc_map.rb # lib/solargraph/gem_pins.rb # lib/solargraph/pin_cache.rb # lib/solargraph/rbs_map/conversions.rb # lib/solargraph/shell.rb # lib/solargraph/workspace.rb # lib/solargraph/yardoc.rb # spec/pin_cache_spec.rb # spec/workspace/gemspecs_resolve_require_spec.rb
FlowSensitiveTyping#process_or never computed any true-branch narrowing for an ||-joined condition, so a check like e.is_a?(Array) || e.is_a?(String) left e at its full declared type inside the if body instead of narrowing to Array | String. Add process_or_isa_union, which narrows only when both sides of the || are plain is_a? calls on the same variable, using the same process_facts/find_var/ComplexType.parse primitives process_isa already uses. Any other shape (different variables, or a side that isn't a plain is_a? call) is left unnarrowed rather than guessed at.
These four suppressions came from castwide#1252's own branch history, authored against an earlier state of this codebase. The underlying gaps they suppressed are already fixed on this integration branch by other, unrelated PRs, so Solargraph's own typecheck now flags them as unneeded. Verified per-line with a full-project typecheck (not per-file, which gives false positives from missing Thor DSL context): no problem reproduces at any of the four spots. A fifth candidate (shell.rb's "flow sensitive typing needs to handle 'raise if'") did reproduce a real error once removed, so it's kept.
Pin::Parameter#compatible_arg? rejected any argument against an RBS-interface-typed parameter (e.g. Hash#fetch's _Key) unless the argument's type was nominally included via CoreFills::INCLUDES. Since Hash#fetch's non-block, non-default overload takes a _Key-typed parameter, no overload of a plain h.fetch(k) call ever matched, and Pin::Method#return_type fell back to unioning every overload's return type together - including the unbound generic X from the two overloads that require a default value or a block. Add :allow_unmatched_interface to the rules passed to conforms_to? in compatible_arg?, matching the leniency TypeChecker itself already applies by default (see Rules#require_interfaces_resolved?) everywhere except the :alpha level. Fixes castwide#1227
A splat lhs entry in a masgn (command, *args = mutator) parses as s(:splat, s(:lvasgn, :args)). The lookup location was computed from the splat node itself, one column off from where the inner lvasgn pin actually lives, so mass_assignment was never set and the splat variable stayed permanently untyped.
…onformance-based `raise` is not its own AST node type in the parser gem - it parses as a plain method call (:send), unlike return/next/redo/retry which are real node types. FlowSensitiveTyping#always_leaves_compound_statement? checked for a :raise node type that never occurs, so a guard like `raise 'no' if x.is_a?(Array)` never narrowed x's type for the rest of the method: exclude_return_type stayed nil and ComplexType#exclude was never invoked with real data. Separately, ComplexType#exclude and UniqueType#exclude used plain array subtraction (items - exclude_types.items), which only removes members equal by ==/hash to something in the exclude list. That misses excluding a parameterized member (Array<Symbol, Array>) via its plain form (Array), the same conformance gap intersect_with already avoids by matching via conforms_to? instead of equality. Fixed both #exclude implementations to reject a member when it conforms to one of the excluded types - narrower members are excluded by a broader exclude type, but not the reverse (excluding Array<Integer> leaves a plain Array member alone).
Pin::BaseVariable treats every variable as lexically scoped, giving up once the reading closure's namespace walk finds no match. Ancestor ivar-assignment candidates from get_instance_variable_pins were discarded, since a descendant class is never lexically nested inside the ancestor class that defines it. InstanceVariable now accepts a pin once the walk bottoms out with no match, and only enforces same-file placement when presence (flow-sensitive narrowing) is actually set. Fixes castwide#1261
ApiMap#get_method_stack and #inner_get_methods both parsed their rooted_tag with ComplexType.parse. That tag is inference-derived, so a type Solargraph itself reconstructed badly raised ComplexTypeError out through Chain#infer and killed the whole `solargraph typecheck` run rather than producing a diagnostic for the one file. Use try_parse, which is what the sibling #get_methods already does with the same parameter: an unparseable tag resolves to undefined, the namespace lookup finds nothing, and the caller gets an empty method stack. Static constants elsewhere (VOID, SYMBOL, ROOT and friends) keep ComplexType.parse - those parse literals, where a raise is a real bug report and a silent undefined would hide it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
…h 2026-08-04 # Conflicts: # lib/solargraph/parser/parser_gem/node_chainer.rb # lib/solargraph/parser/parser_gem/node_methods.rb # lib/solargraph/parser/parser_gem/node_processors/block_node.rb # spec/parser/node_processor_spec.rb # spec/type_checker/levels/strong_spec.rb
process_macro/process_directive's return values were assigned to result, shadowing the outer pins.map block's own result= target. castwide#1319's flow-sensitive reassignment tracking picked up the inner block-local as though it were the outer one, making the outer result unresolvable at its own use site (line 432) and its downstream nil-guards report as unneeded.
…anch 2026-08-04 # Conflicts: # .rubocop_todo.yml # lib/solargraph/complex_type.rb # lib/solargraph/complex_type/unique_type.rb # lib/solargraph/rbs_map/conversions.rb # lib/solargraph/rbs_translator.rb # spec/type_checker/levels/strong_spec.rb
…-08-04 # Conflicts: # lib/solargraph/source/chain/call.rb
…branch 2026-08-04 # Conflicts: # spec/rbs_map/conversions_spec.rb
A guarded, bare accessor call (e.g. return nil if steps.nil?) narrows repeated/direct calls to that accessor correctly, but assigning the guarded value into a fresh local (local = steps) lost the narrowing: local's inferred type still carried nil. Root cause: Pin::BaseVariable#return_types_from_node excludes a variable's own in-progress pin from the candidates used to resolve its RHS (so a = a or index += 1 doesn't try to resolve against the not-yet-computed value being derived). It does this via candidate.assignments.include?(parent_node), which uses Array#include? and therefore Parser::AST::Node#==, which compares nodes structurally (type + children) rather than by identity. Two syntactically identical but unrelated call nodes - e.g. the steps call inside an earlier steps.nil? guard, and the steps call on the right-hand side of local = steps - compare equal even though they are different AST nodes at different source locations. This caused the self-exclusion logic to also incorrectly exclude the flow-sensitive-typing pin synthesized from the guard, discarding its narrowing when resolving local's assignment. Fix: compare by identity (equal?) instead of ==. Adds regression specs: - spec/parser/flow_sensitive_typing_spec.rb: narrowing propagates into a fresh local for both .nil? and truthy-guard forms. - spec/type_checker/levels/strong_spec.rb: strong-level typecheck accepts a declared non-nil @type on such a local.
…gration branch 2026-08-04 # Conflicts: # lib/solargraph/parser/parser_gem/node_methods.rb
…k) into integration branch 2026-08-04
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Integration branch merging in open PRs for combined CI testing.
PRs included
rbs validatea blocking CI check again castwide/solargraph#1317 — Makerbs validatea blocking CI check again