Skip to content

Integration test: 2026-08-04 - #49

Draft
apiology wants to merge 376 commits into
masterfrom
2026-08-04
Draft

Integration test: 2026-08-04#49
apiology wants to merge 376 commits into
masterfrom
2026-08-04

Conversation

@apiology

@apiology apiology commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Integration branch merging in open PRs for combined CI testing.

PRs included

apiology and others added 7 commits August 6, 2026 13:28
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
@apiology

apiology commented Aug 6, 2026

Copy link
Copy Markdown
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

@apiology apiology closed this Aug 6, 2026
@apiology apiology reopened this Aug 6, 2026
apiology and others added 20 commits August 6, 2026 15:13
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.
apiology and others added 17 commits August 21, 2026 16:20
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
…ation branch 2026-08-04"

This reverts commit 42d8c9e, reversing
changes made to 633306a.
…-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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant