Integration test: 2026-08-04 - #49
Draft
apiology wants to merge 263 commits into
Draft
Conversation
RBS allows a union as one member of an intersection - `(A | B) & C`
is valid RBS and means "a value that is A-or-B, and is also C." The
prior conjuncts: Array<UniqueType> couldn't represent that: every
conjunct was forced through UniqueType.parse, so RbsTranslator's
string-based join('&') flattened a nested Union member into a plain
comma list that re-parsed as a top-level union of the whole
expression rather than a nested one.
conjuncts is now Array<ComplexType>, the same type UniqueType's own
subtypes/key_types already use for "this slot holds a full type
expression, which might be a union." A single type is just the
common case of a one-item ComplexType, and since Intersection is
itself a UniqueType (which already fits inside a ComplexType's
items), a conjunct can also be - or contain - another Intersection
with no new plumbing.
RbsTranslator#to_complex_type now builds the Intersection directly
from each member's own recursively-translated ComplexType for
RBS::Types::Intersection nodes, instead of flattening through
type_to_tag's string join. This fixes the (A | B) & C case: to_rbs
now correctly renders `(::A | ::B) & ::C`, and conforms_to? handles
a union conjunct with real union semantics (every member must
conform) rather than losing the grouping.
Conformance#conforms_to_intersection_expectation? no longer needs to
wrap each conjunct in ComplexType.new([conjunct]) before checking it,
since conjuncts are already ComplexTypes.
Added specs for:
- Operator precedence (`&` binds tighter than `,`/union, regardless
of which comes first in the string - matching RBS's documented
"A & B | C is (A & B) | C").
- The parenthetical edge cases this raises: `Array(A, B) & C` (the
existing fixed-tuple-parameter syntax, unaffected) vs a bare
`(A, B) & C` (which reads as an intersection with an anonymous
tuple conjunct, not a grouped union - Solargraph's tag-string
grammar has no standalone grouping syntax).
- Nested union/intersection translation via RbsTranslator: a union as
either conjunct, and nested intersections flattening correctly.
- The resulting known limitation: the informal tag/to_s string for a
nested-union conjunct isn't round-trippable through
ComplexType.parse (there's nowhere to put the grouping), while
to_rbs's real RBS syntax round-trips correctly through RBS's own
parser. Documented with a spec rather than left as a surprise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
Given `t` declared as `T` and a runtime guard `t.is_a?(M)` where M is
a mix-in with no visible relationship to T, narrow_with previously
found no compatible pair in its cross-product and fell back to
UNDEFINED - discarding both facts we'd actually learned about `t`,
even though a value can perfectly well be both (any class can pick
up any module, whether or not it's declared in code Solargraph can
see). The correct narrowed type is `T & M`.
Building an intersection unconditionally whenever neither side
conforms to the other turned out to be unsafe and broke real,
previously-correct behavior in two ways, both caught by existing
specs:
- Two different concrete classes can never describe the same value
(an object has exactly one class), so combining sibling subclasses
from a declared union (e.g. narrowing `Repro1, Repro2` via
`is_a?(Repro1)`) produced a nonsensical `Repro2 & Repro1` for the
pairing that should have just been dropped.
- Defaulting to "build an intersection when uncertain" fired for
synthetic/unresolvable names too (e.g. `Boolean`, which isn't a
real indexed class), pulling in types from unrelated parts of a
method's signature that had nothing to do with the guard being
narrowed.
So the new mixin_pairing? check is deliberately conservative: only
build the intersection when at least one side is *positively
confirmed* to be a module via a new namespace_kind lookup
(api_map.get_path_pins(...).find { Pin::Namespace }.type). Everything
else - two classes, or anything unresolvable - falls back to the
original drop-the-pair behavior exactly as before.
Verified against real tooling before implementing: TypeScript
resolves an intersection of incompatible primitives (`string &
number`) to `never`, and Steep doesn't build an intersection at all
for either case (it substitutes the checked type wholesale). Our
approach preserves more information than Steep's for the specific
case it targets (declared class + mix-in), while still avoiding the
uninhabited-type problem TypeScript's `never` answers for classes -
we just don't have real bottom-type infrastructure to produce that
answer, so unrelated concrete classes fall back to UNDEFINED as
before rather than a proper bottom.
Also adds two pending spec files documenting related, explicitly
out-of-scope gaps raised while working through this, so they're
tracked rather than silently unknown:
- spec/complex_type/exclude_spec.rb: ComplexType#exclude already
takes an api_map parameter but never uses it - it only removes
exact matches, not known subtypes of an excluded type.
- spec/complex_type_spec.rb: no api_map-aware union simplification
exists anywhere (`Sup, Sub` never collapses to `Sup` even though
every Sub instance already is a Sup instance).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
The fix for issue castwide#1229 taught to_complex_type to build an Intersection directly from the RBS AST for a *top-level* RBS::Types::Intersection, since a joined string can't represent a union nested inside an intersection (`(A | B) & C`) - there's nowhere in Solargraph's tag grammar to put the grouping. That bypass only covered the one entry point used for method return types and parameter types. Every other place RbsTranslator recursively translates a nested type still went through the old flattening path: RBS::Types::Optional, RBS::Types::Union members, RBS::Types::Tuple elements, and generic type arguments (Array[...], Hash[...], and any other name with type args, via the private build_type/type_tag pair). A plain intersection nested in any of these was fine; the same union-in-intersection grouping got silently flattened wherever it appeared below the top level - confirmed for all of them: Array[(Integer | String) & Comparable] -> Array<Integer, String & Comparable> Hash[Symbol, (Integer | String) & Comparable] -> wrong grouping in both tag and to_rbs ((Integer | String) & Comparable)? -> 3-item union instead of 2 [(Integer | String) & Comparable, Integer] -> 3-element tuple instead of 2 That optional/tuple case is worse than imprecise - it silently changes the shape of the type (extra union member, extra tuple element), not just its grouping. Rather than patch each of these call sites individually, to_complex_type now handles every composite/recursive RBS node directly - Intersection, Optional, Union, Tuple, and (via build_unique_type) ClassInstance/ Alias/Interface/ClassSingleton generic arguments - building the ComplexType/UniqueType object graph by recursing through itself, the same way the Intersection case already did. type_to_tag is left with only the leaf cases that can't contain a nested type (literals, bool, nil, void, generics, self/instance, Proc, etc.), where a tag string is unambiguous and always was fine. This also deletes the private build_type/type_tag pair in favor of the existing (and already correct) but previously unused public build_unique_type - it already built generic type arguments by recursing through to_complex_type rather than stringifying them; the private duplicate that actually got called had regressed to the lossy string path. One method, already fixed, was simply dead code. Adds spec/rbs_translator_spec.rb covering the whole class of position this affects, not just the one reported: a control case (plain intersection nested in a generic argument, already correct), and the seven broken positions above plus a doubly-nested case, all now verified to preserve grouping correctly via to_rbs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
Widget & Comparable did not conform to a freshly-parsed Widget & Comparable unless Widget already happened to include Comparable - reported as a comment on PR castwide#1231, where it was misdiagnosed as a macro-substitution / object-identity problem. It isn't: it reproduces with two plain ComplexType.parse calls and zero macro machinery. Root cause: Intersection#conforms_to? always decomposed the inferred side first - "does any ONE of my conjuncts, checked alone, satisfy the whole expected type?" - before knowing whether the expected side was itself an intersection. Checking a single conjunct (e.g. Widget alone) against an expectation that itself requires satisfying two things (Widget & Comparable) demands that one conjunct cover both, which fails whenever the conjuncts don't already relate to each other - even when the inferred and expected types are identical. The correct rule for A & B <: C & D is that every conjunct of the expected side must be satisfied by *some* conjunct of the inferred side, not necessarily the same one each time. conforms_to? now detects that shape via a new sole_intersection helper and composes correctly for it, falling through to the previous logic otherwise. Deliberately scoped to the shape all existing tests and the report cover - expected consisting of exactly one Intersection - rather than also guessing at the semantics of a union with an intersection as just one of several alternatives. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
Fred raised two additional examples on PR castwide#1223: a scalar reassignment union that still shows the stale pre-+= literal, and a plain Array's inferred element type not tracking a later #push. Both reproduce identically on master, so neither is caused by this PR. The first is the same general "sequential assignment" flow-narrowing gap already tracked as pending since PR castwide#863 (see the pre-existing "replaces type with reassignments" spec). The second is the same "no mutation tracking" limitation already documented and accepted for tuples/#unshift in this PR, generalized to plain arrays via the separate literal-array inference path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCTZUQv2ijSU5Cqgw4CU1f
Pin::BaseVariable#probe unions the return types of every assignment to a variable in scope. When one assignment's type is a literal (e.g. `0` from `x = 0`) and another is that literal's own non-literal base type (e.g. `Integer` from `x += 1`, which already correctly widens away the literal per the earlier reassignment fix), the literal adds no information the base type doesn't already carry - keeping both just reads as if the literal value were still reachable after a later, wider assignment. Drop such redundant literal items so `x = 0; x += 1; x` infers as `Integer` instead of `0, Integer`. This does not touch the general "sequential assignment" narrowing gap (unioning across *all* assignments regardless of position, tracked since PR castwide#863) - it only removes items that were always redundant given another item already in the same union. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCTZUQv2ijSU5Cqgw4CU1f
Array's mutating methods that can shift, replace, or reorder a tuple's existing positions (unshift/prepend, insert, delete_if, keep_if, reject!, select!/filter!, compact!, flatten!, uniq!, sort!, sort_by!, reverse!, rotate!, shuffle!, replace, fill, clear, collect!/map!) were inherited from core Array unmodified, so their RBS-declared `-> self` return type kept the precise (and, after such a call, wrong) Tuple type. #push/#<</#concat are deliberately excluded - appending past the known arity can't invalidate an already-known position. Overriding these to return the widened, position-erased `Array[union-of-all-elements]` type instead of `self` means capturing the result via reassignment (`array = array.unshift(x)`) now falls back to the safe union, reusing this PR's existing reassignment-tracking machinery. It does nothing for the more common bare-statement form (`array.unshift(x)`, no reassignment) - that's still the documented castwide#1196 scenario-4 limitation, unaffected and still covered by its own spec. Note for reviewers: RBS's own maintainers hit this same wall and retreated from it for the general core-type case - see ruby/rbs@aae95840 ("Use monomorphic versions of in-place modifying methods"), which reverted an earlier attempt at a non-self, generically-typed #collect! because "it's not possible to represent side-effects and the receiver type changing." Our case differs in a way that keeps it sound: we're not introducing a fresh polymorphic type var the way that attempt did, just narrowing to a type that's already a strict superset of every possible resulting position - the tuple's own declared union. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCTZUQv2ijSU5Cqgw4CU1f
TypeChecker#signature_argument_problems_for used to bail out on any signature with a restarg parameter, skipping type checking entirely for the rest of the call. That's why `y = [1]; y.push 'two'` (Fred's second example on castwide#1223) went unflagged even though `push` expects an Integer. Restarg params are now checked argument-by-argument against the restarg's declared type, resolved against the receiver's actual generic parameters (e.g. `Integer` for an `Array<Integer>` receiver). Trailing positional parameters and an implicit kwargs hash appended to the call's arguments are excluded from the restarg's own checks. This surfaced a real bug in RbsTranslator#to_parameter_pin: restarg and kwrestarg parameters had their per-element type discarded and hardcoded to bare `Array` / `Hash{Symbol => Object}`, so there was never any element type to check against in the first place. Fixed to preserve the real per-element type, falling back to the old bare Array/Hash only when the element type is genuinely untyped (e.g. an inline `#: (*bar) -> bool` annotation with no declared element type). Two specs in spec/pin/method_spec.rb asserted the old erased-to-bare behavior and are updated to reflect the now-tracked type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCTZUQv2ijSU5Cqgw4CU1f
CI runs the matrix against RBS 3.10.0 through 4.0.2, where Array#push's restarg parameter is named differently (e.g. `obj`) than in the RBS version used locally (`objects`). The parameter name is an incidental detail of the core RBS declaration, not something this PR's type-checking logic controls, so match on the substance of the message instead of the exact name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCTZUQv2ijSU5Cqgw4CU1f
lsegal/yard#1700 proposes standardizing `|` as an explicit union operator and `[...]` as a grouping construct for YARD type tags, alongside the `&` intersection operator this branch already added for solargraph#1229. Implementing the full syntax here so Solargraph's own parser and the upstream proposal describe the same grammar, and so `(A | B) & C` - previously only buildable by translating real RBS or constructing an Intersection object directly, per the now-outdated comment on the parentheses spec - has an actual tag-string form. `|` binds looser than `&` (matching RBS's documented precedence) and, inside a fixed-arity context (`Array(...)` tuples, or a generic type's positional parameters), groups multiple types into a single slot instead of splitting into separate positional arguments - the same distinction `,` already makes there. In an implicit-union context (Array<...>/Set<...>, hash key/value lists, the top-level list itself), `|` and `,` land on the same result, since every comma-separated type in those contexts is already unioned regardless of grouping. `[...]` is the actual grouping construct - the only way to mark where a union ends when it needs to be one conjunct of an intersection (`[Foo | Bar] & Baz`). It's deliberately conservative about when it opens: only at a fresh atom (blank base, not already nested in <>/{}/()), otherwise `[`/`]` are ordinary characters - this matters for quoted string-literal types like `"[]"`, which have no concept of grouping and would otherwise crash self-typecheck against the real Dir RBS core stub. Also fixes the anonymous shorthand forms `<A>`, `(A)`, `{A=>B}` (typed before this as an empty-name UniqueType) to default their name to Array/Array/Hash respectively, per YARD #1700's third documented change - so an anonymous form now behaves exactly like its named equivalent, including for rooting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
Part of re-enabling `solargraph typecheck --level strong` in CI (currently `continue-on-error: true`, 496 pre-existing problems). - Remove 42 `@sg-ignore` comments strong-mode now reports as unneeded (the underlying issue they suppressed no longer exists). - lib/solargraph/source/chain/literal.rb: reword nested `@sg-ignore` mentions inside a commented-out illustrative code block so Solargraph's comment parser doesn't mistake them for live annotations (was causing a false "unneeded @sg-ignore" report with no matching live comment to remove). - lib/solargraph/yardoc.rb: keep one @sg-ignore in place (reworded) for an Open3.capture2e overload-resolution edge case strong mode can't otherwise clear; removing it surfaced a real "Unresolved call to success?" report. spec/pin/combine_with_spec.rb's 5 stale `pending` markers are intentionally NOT removed here: they only start passing once PR castwide#1238's Pin::Method#combine_same_type_arity_signatures fix is present, and that fix is being kept in a separate, non-annotation PR. Removing them on this branch (which doesn't have that fix) would turn 'pending' into a real failure. Verified: typecheck strong (497 -> 454 problems, no new problems introduced, no regressions). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit cb7bb61)
Continues re-enabling `solargraph typecheck --level strong` in CI. Root-cause fixes (not suppression): - lib/solargraph/rbs_translator.rb, lib/solargraph/rbs_map/conversions.rb: `RbsTranslator.to_complex_type`/`type_to_tag` were tagged `@param type [RBS::Types::Bases::Base]`, but RBS itself defines no such shared base type -- `RBS::Types::t` (RBS's own "any type" alias) is a flat union of ~20 concrete classes. Retagging both methods with that full union fixed 11 identical false-positive reports in one shot. Same fix applied to `RbsTranslator.to_parameter_pins` / `Conversions#extract_method_type_return_type`, which are genuinely called with either `RBS::MethodType` or `RBS::Types::Block` (both just need `.type`). - lib/solargraph/rbs_map/conversions.rb: removed two entirely dead, shadowed method definitions (`build_type`, `parts_of_function`) -- each had an earlier, unreachable definition still calling two methods (`method_type_to_type`, `other_type_to_type`) that don't exist anywhere in the codebase. Ruby silently uses the later definition, so this was always dead code, not a live bug, but it's why rubocop's Lint/DuplicateMethods was already flagging this file. - rooted_name/fqns/build_type: replaced `Hash#fetch(key, default)` (whose two-arg overload Solargraph can't resolve generically here) with `Hash#[] || default`, which type-checks correctly. - Real nil-safety fixes (guard rewritten so flow typing can see it, not suppressed): node_methods.rb's paren-scanning method-signature parser (String#[] with a Range is nilable even though the surrounding bounds checks make it unreachable in practice); location_decl_to_pin_location / RbsTranslator.to_sg_location's `location&.name.nil?` guards, rewritten as `location.nil? || location.name.nil?` so Solargraph narrows `location` afterward. Suppressions (matching this codebase's established @sg-ignore conventions, used where the gap is in Solargraph's own flow-typing engine, not a bug in this code -- see the categorized backlog in lib/solargraph/type_checker/rules.rb): - `Parser.is_ast_node?(x)`-style custom predicate wrappers don't narrow `x` for later calls (flow sensitive typing needs to narrow down type with an if is_a? check). - Postfix `unless x.nil?` guards on a repeated subexpression don't narrow the repeated use (Translate to something flow sensitive typing understands). - `if obj.attr` doesn't narrow a later `obj.attr` re-access (flow sensitive typing needs to handle attrs). - `case type; when SomeClass; type.foo` doesn't narrow `type` per branch (flow sensitive typing should support case/when). - A few pre-existing, unrelated-to-narrowing "Unresolved call" reports on RBS-derived types Solargraph can't otherwise resolve. spec/pin/method_spec.rb: switch the batch-1 regression test to `instance_double` (RSpec/VerifiedDoubles), fixing a rubocop failure CI caught on the batch-1 push. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (454 -> 374 problems this batch; 497 -> 374 overall across both batches). Note: CI's `run_solargraph_rspec_specs` job (solargraph-rspec's own integration suite, run against this branch) shows 3 pre-existing failures. Confirmed via local bisection (pointing solargraph-rspec's Gemfile at a pristine, unmodified castwide/solargraph master checkout) that these same failures reproduce on master itself, and confirmed via `gh run list` that this job has already failed on a master push independent of this PR. Not caused by, or fixable within, this PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit e54709d)
Continues re-enabling `solargraph typecheck --level strong` in CI.
- lib/solargraph/type_checker.rb: fully clean (27 -> 0 problems).
Real fixes: `kwarg_problems_for` now returns early if
`sig.parameters[idx]` is nil (was calling `.name`/`.decl`/
`.asgn_code` on a possibly-nil param without a guard);
`arity_problems_for` now falls back to `[]` if
`pin.signatures.map { ... }.first` is nil (empty signatures list).
The rest are `@sg-ignore`s matching this codebase's established
flow-typing-gap conventions (postfix nil guards, attr re-access,
Hash `||=` on a key, `Array#last` after an emptiness check).
- lib/solargraph/rbs_translator.rb: wrap the two `@param type [...]`
union-type tags (added in batch 2) in
`rubocop:disable/enable Layout/LineLength` -- CI's rubocop check
caught these on the batch 2 push (511/513 chars vs the 224 limit).
Verified: full rspec suite (1618 examples, 0 failures, 60 pending)
and typecheck strong (374 -> 347 problems this batch; 497 -> 347
overall across three batches).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit d940789)
Continues re-enabling `solargraph typecheck --level strong` in CI. lib/solargraph/pin/method.rb: fully clean (17 -> 0 problems). Real fixes: - `return_type_from_inline_rbs` / `signatures_from_inline_rbs`: guard against `RBS::Parser.parse_method_type` returning `nil` (its own RBS signature allows this independent of raising `RBS::ParsingError`, which is the only failure mode these methods previously handled). - `dodgy_visibility_source?`: add a `@return [Boolean]` tag (was missing, same "return type could not be inferred" pattern already fixed for `splatted_hash?` in batch 2). The rest are `@sg-ignore`s matching this codebase's established flow-typing-gap conventions from lib/solargraph/type_checker/rules.rb (String#[] with a Range being nilable despite surrounding bounds checks, Array#first/#last after an emptiness check, attr re-access after a truthy check, Hash `||=` on a key). One (`Macro.from_directive` called with an already-built `Macro` instead of a raw `YARD::Tags::Directive`) works at runtime only because `Macro` duck-types `#tag` the same way -- confirmed by reading both classes before suppressing rather than assuming. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (347 -> 330 problems this batch; 497 -> 330 overall across four batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 3262e75)
Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/parser/parser_gem/node_chainer.rb: fully clean (15 -> 0 problems). - lib/solargraph/parser/parser_gem/node_processors/send_node.rb: fully clean (17 -> 0 problems). Both files are almost entirely `node.children[N]` accesses feeding into recursive chain-building calls (`NodeChainer.chain`, `generate_links`) after guards Solargraph's flow typing doesn't propagate (`is_a?` checks, truthiness checks, or just structural guarantees from the parser's own AST shape) -- `@sg-ignore`s matching the established "Need to add nil check here" convention. One real (harmless) restructuring: `NodeChainer#generate_links`'s `:or` branch built a two-element array inline (`[NodeChainer.chain(n.children[0], ...), NodeChainer.chain(n.children[1], ...)]`), which put both nilable-argument call sites on the same logical statement -- Solargraph could only attribute one `@sg-ignore` to it. Split into two local variables assigned separately so each call site gets its own annotation; no behavior change. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (330 -> 298 problems this batch; 497 -> 298 overall across five batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 3960c11)
…y clean Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/parser/comment_ripper.rb: fully clean (15 -> 0 problems). All from the same root: Ripper's `result` tuple is declared `Array(Symbol, String, Array(...))`, but Solargraph doesn't narrow positional `result[N]` indexing to each tuple slot's specific type -- every index resolves to the full element-type union instead. `@sg-ignore`s matching this file's existing convention for the identical pattern. - lib/solargraph/parser/flow_sensitive_typing.rb: fully clean (13 -> 0 problems). Same nil-narrowing gaps as prior batches (`@type` tags asserting non-nil on values Solargraph itself infers as nilable from `node.children[N]`; a nested generic Hash/Array value type Solargraph can't fully resolve). Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (298 -> 270 problems this batch; 497 -> 270 overall across six batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 319dea9)
Continues re-enabling `solargraph typecheck --level strong` in CI. lib/solargraph/library.rb: fully clean (12 -> 0 problems). Real fixes: - `references`: `[api_map.source_map(filename)]` could contain a nil element (source_map returns nil if the file isn't mapped); `.compact` it before iterating, avoiding a latent `NoMethodError` on `nil` if that branch were ever hit with an unmapped file. - `next_map`: was writing to and then immediately re-reading from `source_map_hash` to get its own return value, which Solargraph can't see is guaranteed present -- keep the mapped source in a local variable and return that instead of re-fetching from the hash. The rest are `@sg-ignore`s matching established conventions: the `nil`-literal-vs-`NilClass` representation mismatch also seen in batch 4 (`attach nil`, `Bench.new(live_map: ...)`), the `Open3.capture3` overload-resolution gap from batch 4 applied to a second call site, and a few more `Array#shift`/`Hash#[]`-after-a-set nil-narrowing gaps. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (270 -> 258 problems this batch; 497 -> 258 overall across seven batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 4b62fd4)
Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/api_map.rb: fully clean (12 -> 0 problems). - lib/solargraph/language_server/host.rb: fully clean (12 -> 0 problems). Real fixes: - `Host#pending_completions?` was tagged `@return [Bool]` -- not a real YARD/Solargraph type name (should be `Boolean`), so the declared type itself was unresolvable. - `Host#client_supports_progress?` / `#prepare_rename?` had no `@return` tag at all and returned a raw `&&` chain (which could yield a Hash value, not just true/false); added `@return [Boolean]` and wrapped the body in `!!(...)` so the return value is a real boolean, not just type-annotated as one. The rest are `@sg-ignore`s matching established conventions: several more `Hash#[]`-after-a-truthy-check nil-narrowing gaps, the `nil`-literal-vs-`NilClass` mismatch (`Source::Change.new` with a ternary that can yield literal `nil`), and one gap in a third-party gem's return typing (`Diff::LCS.diff`, which doesn't ship strong RBS/YARD types Solargraph can resolve). Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (258 -> 234 problems this batch; 497 -> 234 overall across eight batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit d0cb67a)
Continues re-enabling `solargraph typecheck --level strong` in CI.
lib/solargraph/api_map/store.rb: fully clean (10 -> 0 problems).
Real fixes:
- `get_path_pins`: `index.path_pin_hash[path]` falls back to `[]`
(matches the declared non-nilable `Array<Pin::Base>` return type;
Hash#[] on a missing key is a normal, expected case here, not an
error).
- `fqns_pins`: `fqns_pins_map[[base, name]]` falls back to `[]` too --
the hash has a default proc that always populates the key, so this
never actually returns nil, but Solargraph can't see through
`Hash.new { ... }` default-proc population.
The rest are `@sg-ignore`s matching established conventions:
`Hash#key?`-guard-then-`[]`-fetch not narrowing (same pattern fixed
repeatedly in prior batches, here across `superclass_references`,
`namespace_hash`, `@indexes.last`), and the `nil`-literal-vs-`NilClass`
representation mismatch in a cached Hash assignment expression.
Verified: full rspec suite (1618 examples, 0 failures, 60 pending)
and typecheck strong (234 -> 224 problems this batch; 497 -> 224
overall across nine batches).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 5e8f295)
Continues re-enabling `solargraph typecheck --level strong` in CI.
- lib/solargraph/pin/block.rb: fully clean (9 -> 0 problems).
- lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb:
fully clean (9 -> 0 problems).
- lib/solargraph/diagnostics/rubocop.rb: fully clean (9 -> 0
problems).
Real fix: `Block#destructure_yield_types`'s `parameters.map.with_index
{ ... }` (map called without a block, then chained through
`with_index`) return-typed as `Enumerator` instead of `Array` --
rewritten as the equivalent, more standard
`parameters.each_with_index.map { ... }`, which Solargraph resolves
correctly and matches the declared `Array<ComplexType>` return type.
The rest are `@sg-ignore`s matching established conventions:
`is_a?` checks combined with `&&` in an `if`/`elsif` chain not
narrowing the checked variable for later `.type`/`.children` calls in
sclass_node.rb (same class of gap as the plain single-condition case
fixed in earlier batches, just with more conditions in the same
`if`); repeated `Hash#[]`-chain nil-narrowing gaps parsing RuboCop's
JSON offense output in diagnostics/rubocop.rb.
Verified: full rspec suite (1618 examples, 0 failures, 60 pending)
and typecheck strong (224 -> 197 problems this batch; 497 -> 197
overall across ten batches).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2a262cd)
Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/convention/data_definition/data_assignment_node.rb: fully clean (7 -> 0). - lib/solargraph/convention/struct_definition/struct_assignment_node.rb: fully clean (7 -> 0). - lib/solargraph/convention/struct_definition/struct_definition_node.rb: fully clean (7 -> 0). Real fix, applied identically across the two `*_assignment_node.rb` files (they're structurally the same class, one for `Data.define`, one for `Struct.new`): `node.children[2]` and `node.children[0]` were each re-evaluated 2-3 times across a nil check and subsequent uses. Solargraph doesn't narrow a repeated method-call expression the way it narrows a plain local variable, so each re-access re-triggered the same nilable warning even though the code was already guarded. Extracting each into a local variable once, right after computing it, lets Solargraph's ordinary local-variable nil-narrowing do its job instead of suppressing each repeated access individually. The remaining occurrences (mostly in `struct_node`/`data_node` private helper methods that intentionally re-derive from `node` without a preceding nil check, and a few multi-level `.children[0]` chains) are `@sg-ignore`s matching this codebase's established conventions. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (197 -> 176 problems this batch; 497 -> 176 overall across eleven batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit e51fe1f)
Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/doc_map.rb: fully clean (8 -> 0 problems). - lib/solargraph/pin/callable.rb: fully clean (7 -> 0 problems). - lib/solargraph/source.rb: fully clean (7 -> 0 problems). All `@sg-ignore`s matching this codebase's established conventions from earlier batches: `Hash#key?`-guard/`Hash#[]=`-then-fetch not narrowing, the `Open3.capture3` overload-resolution gap (a third call site, same as batches 4 and 7), `||=` on a Hash key not narrowing, and the `nil`-literal-vs-`NilClass` representation mismatch. One case in `source.rb` also carries a real type-hierarchy gap Solargraph can't see: `Parser::AST::Node` is a subclass of the `ast` gem's `AST::Node`, but nothing tells Solargraph about that relationship, so a method declared to return `AST::Node` that actually returns a `Parser::AST::Node, nil` needs suppressing on both counts. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (176 -> 154 problems this batch; 497 -> 154 overall across twelve batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 287fff7)
Continues re-enabling `solargraph typecheck --level strong` in CI.
- lib/solargraph/complex_type.rb: fully clean (7 -> 0 problems).
- lib/solargraph/complex_type/unique_type.rb: fully clean (7 -> 0
problems).
Real fix: `ComplexType#expand` and `UniqueType#expand` had no
`@param`/`@return` tags at all; added `@param named_types
[Hash{String => UniqueType}]` / `@return` tags matching how they're
actually used (`named_types[name] || self`).
The rest are `@sg-ignore`s matching established conventions:
`Array#first`/`Array#[]` on `@items` treated as guaranteed-present
(a ComplexType always wraps at least one UniqueType) but not
provable statically, and the `nil`-literal-vs-`NilClass`
representation mismatch.
Verified: full rspec suite (1618 examples, 0 failures, 60 pending)
and typecheck strong (154 -> 140 problems this batch; 497 -> 140
overall across thirteen batches).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 246d72b)
Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/pin_cache.rb: fully clean (6 -> 0 problems). - lib/solargraph/pin/base.rb: fully clean (6 -> 0 problems). - lib/solargraph/yard_map/mapper/to_method.rb: fully clean (6 -> 0 problems). - lib/solargraph/shell.rb: fully clean (6 -> 0 problems). Real fixes: `Pin::Base#macro_names` and `#collect_macro_names` had no `@return` tag at all; added `@return [Array<String>]` matching their actual behavior. `Shell#rbs` (a Thor CLI command) had no `@return` tag either; added `@return [void]`. The rest are `@sg-ignore`s matching established conventions, including a new instance of the `FileUtils::path` RBS type-alias gap (6 call sites across pin_cache.rb and shell.rb -- `FileUtils::path` is an RBS type alias for a String/Pathname union, but Solargraph doesn't resolve the alias against a literal String argument) and the `choose_pin_attr_with_same_name` dynamic-`send`-based generic return gap already seen for its sibling `choose_pin_attr` in batch 12. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (140 -> 117 problems this batch; 497 -> 117 overall across fourteen batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 5f63ed2)
…specs Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/api_map/cache.rb: fully clean (5 -> 0). - lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb: fully clean (5 -> 0). - lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb: fully clean (5 -> 0). - lib/solargraph/source_map/clip.rb: fully clean (5 -> 0). - lib/solargraph/workspace/gemspecs.rb: fully clean (5 -> 0). Real fixes: - `Cache#get_methods`/`#get_constants`/`#get_receiver_definition` are Hash-backed cache lookups that can genuinely miss (declared non-nilable but a `Hash#[]` cache read can return nil) -- widened their `@return` tags to include `nil`, matching how their one caller (`ApiMap#get_methods`, `unless cached.nil?`) already treats them. - `NamespaceNode#parameters_from_inline_rbs`: replaced a guard-then-repeated-access on `match[1]` with a local variable so Solargraph's ordinary nil-narrowing applies. - `ResbodyNode#process`: same fix for `node.children[1]`, reused across four lines in the method. - `Workspace::Gemspecs#gemspec_or_preference`: same `preference_map` `Hash#key?`-guard pattern already fixed in `DocMap` (batch 12) -- this is a separate, similarly-named method in a different class. The rest are `@sg-ignore`s matching established conventions, including a fourth `Open3.capture3` overload-resolution gap site. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (117 -> 92 problems this batch; 497 -> 92 overall across fifteen batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit c1e5db8)
Extract per-overload signature matching in Call#inferred_pins into match_overload_type, improving how argument/block types are matched against method overloads and how macro/directive-based pins are reprocessed when no signature matches by type alone. Extracted from castwide#1006 (Improve pin caching) as a standalone piece: this is a type-inference improvement to method call resolution, independent of the gem pin caching machinery in the rest of that PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… nodes Continues re-enabling `solargraph typecheck --level strong` in CI. Note: this branch's original base already called simple_resolve(name, mixin, internal) in Constants#complex_resolve's mixin-recursion branch (a pre-existing bug: simple_resolve only resolves one gate, unlike resolve(name, mixin), which recurses through resolve_and_cache across all of mixin's own ancestry, so multi-hop transitive constant resolution silently broke -- e.g. Module4 includes Module3 includes Module2 includes Module1, with a constant assigned in Module2 referenced from Module4). castwide/master fixed this independently in castwide#1234 ('resolves remote constants'), which also added a regression test for it, after this branch was created. Since this consolidation branch is built on current master, that fix is already present; keeping master's resolve(name, mixin) as-is here rather than reintroducing the stale simple_resolve call via this cherry-pick's patch context. Kept the rest of this commit's sg-ignore comments and annotation fixes elsewhere in this file/batch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 035705ca4d2d7f00c5a67e0c9a24f81f14fa789e)
…, ParseDirective Continues re-enabling `solargraph typecheck --level strong` in CI. - lib/solargraph/parser/node_processor.rb: fully clean (3 -> 0). - lib/solargraph/parser/parser_gem/node_processors/args_node.rb: fully clean (3 -> 0). - lib/solargraph/source/chain.rb: fully clean (3 -> 0). - lib/solargraph/source_map.rb: fully clean (3 -> 0). - lib/solargraph/yard_map/directives/parse_directive.rb: fully clean (3 -> 0). All `@sg-ignore`s matching established conventions from earlier batches: `||=` on a class variable Hash not narrowing, `Array#last` treated as guaranteed-present, generic-method (`_locate_pin`) downcasts to specific return types, and the `nil`-literal-vs-`NilClass` ternary mismatch. Verified: full rspec suite (1618 examples, 0 failures, 60 pending) and typecheck strong (72 -> 57 problems this batch; 497 -> 57 overall across seventeen batches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 6d0b3ad)
…e hard-fail
Closes the last 41 problems, bringing `solargraph typecheck --level
strong` from 497 problems (when this PR started, method stubbed) to
0. Also removes `continue-on-error: true` from the typecheck CI step
(the `@todo Temporary, expect to revert in 0.60` this PR has been
working toward since batch 1) -- strong mode is now a real, enforced
gate again, not just informational.
18 files hit real fixes:
- `Workspace#source` / `#synchronize!`, `YardMap::Cache#get_path_pins`,
`YardMap::Mapper#macros_for_method_object`: Hash-backed lookups
declared non-nilable but genuinely can miss -- widened return types
or added `|| []`/`|| default` fallbacks matching how callers
already treat them.
- `Host::Message.select`, three `set_result nil` call sites,
`RbsMap#short_name`, `Source::Chain::Literal#value`: missing or
wrong `@return`/`@param` tags (a literal `[Bool]` typo, an
`attr_reader` with no declared type at all).
- Five identical `closure_at` methods across
yard_map/directives/{attribute,domain,method,override,visibility}_directive.rb
shared the exact same `Array#select.last` pattern already root-caused
and fixed once for parse_directive.rb in batch 17.
The remaining ~30 files are `@sg-ignore`s matching every convention
established across this PR's 18 batches: `Hash#[]`/`Array#last`
guard-then-fetch not narrowing, the `nil`-literal-vs-`NilClass`
mismatch, the `Open3.capture3` overload-resolution gap (two more
sites), and the `FileUtils::path` RBS type-alias gap (now also fixed
in this repo's own Rakefile, which the strong-mode target apparently
covers too).
Verified: full rspec suite (1618 examples, 0 failures, 60 pending)
and typecheck strong: 41 -> 0 problems in 250 files, exit code 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 1ecdfc7)
…-room verification `Gem::StubSpecification` was unresolvable as a constant in a freshly bundled, freshly `rbs collection install`-ed environment (Docker ruby:4.0, matching the CI recipe exactly), even though this repo's long-lived local development bundle didn't hit it -- this repo has no committed Gemfile.lock or rbs_collection.lock.yaml (both gitignored), so every fresh install resolves whatever gem/RBS versions are current at that moment. `@sg-ignore` matching this PR's established pattern for RBS-resolution gaps in `case`/`when`. Caught by re-verifying the final batch in a brand-new Docker container + fresh clone, rather than trusting the long-lived local bundle this whole PR was developed against -- worth flagging as a real (if narrow) source of CI flakiness independent of any code change here, since a *different* constant could equally fail to resolve on a different day depending on what gem_rbs_collection's `main` branch or RubyGems' own RBS core sigs look like at that moment. Verified in a fresh Docker clean-room (bundle install + rbs collection install from scratch, matching CI): typecheck strong 0 problems, exit 0. Full rspec suite: 1618 examples, 0 failures, 60 pending. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 6a4961f)
BaseVariable#definite_reaches? no longer compares a query Location against a separately-stored conditional_override_boundary Range. Instead it checks whether the location falls within this pin's own compound_statement's location range - the CompoundStatement pin already carries that range, and since a nested CompoundStatement's location is always a subrange of its parent's, this single containment check already accounts for arbitrarily nested branches without needing to walk the chain further. This removes the duplicate bookkeeping the original PR 1282 fix introduced: Region#conditional_boundary (a Range) and BaseVariable#conditional_override_boundary are gone, along with the Range.from_node(...) computation every conditional-construct node processor performed to populate them - that range is now read directly off the compound_statement pin instead of being computed a second time. lvasgn_node.rb's `definite` computation goes back to a plain Region#conditional boolean rather than `conditional_boundary.nil?` (and was briefly, incorrectly, tried as `compound_statement.is_a? (Closure)` during this rewrite - reverted because a block's body pin IS a Closure, for variable-scoping purposes, despite running zero or many times, which is exactly the case `conditional_boundary`/`conditional` exists to distinguish). Every closure-creating node processor (def_node.rb, defs_node.rb, namespace_node.rb) now explicitly resets `conditional: false` for its body, since entering a fresh method/namespace scope always runs its body top-to-bottom regardless of how the closure itself was reached, unlike a block. Added: - A loop-ordering regression test confirming a reassignment inside a while body doesn't affect a reference textually before it. - combine_with specs for Pin::CompoundStatement covering the location-based tiebreak and the nil-vs-non-nil case. Verified: full suite (1638 examples, 0 failures), typecheck self-check diffed against the pre-fix baseline (587 problems vs. 591 baseline - net fewer, since deleting the Range.from_node calls also removed several instances of the pre-existing nilable-AST-child pattern already tolerated throughout these files). Combines what were originally staged as two follow-up PRs into one - see castwide#1282 for the base fix and design discussion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YbhZvdCv7xdziXyKJiPuGk
Add a CompoundStatement parent chain and use it for reassignment override eligibility
Reported at castwide#1223 (comment): on RBS 4.0.x, a single-argument Hash#fetch call resolves K to the receiver's literal key type (e.g. "Index" from a Hash{"Index" => Float} @PARAM tag), leaving Hash#fetch with one candidate overload and no non-literal sibling to fall back to. The exact-literal-match gate added for tuple's literal-indexed overloads (so a non-literal argument falls through to tuple's safe catch-all) rejected that candidate outright, since the calls plain string argument is not itself literal-typed. With no overload matching at all, Call#inferred_pins fell back to the union of every overloads declared return type, leaking generic<X> from fetchs default-value and block overloads into the inferred return type. Chain::Call#inferred_pins now tries the overload list twice: first requiring an exact-literal match (unchanged default behavior), then, only if nothing matched at all, retrying the same overloads without that requirement. Pin::Parameter#compatible_arg? takes a require_literal keyword controlling whether literal_arg_matches? applies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sv19rvjCK7SVSvTCuh3kN2
Region#conditional was a separate boolean threaded alongside compound_statement, requiring every node processor to pass both in lockstep (e.g. block_node.rb: compound_statement: block_pin, conditional: true). Keeping two parallel values in sync at every call site is exactly the kind of duplication this refactor set out to remove, and it's the shape of bug that broke Block handling mid-refactor (definite briefly, incorrectly, derived from compound_statement.is_a?(Closure), which is true for Block despite a block body running zero or many times). conditional is now a constructor attribute on Pin::CompoundStatement itself, set once where each construct is built (Pin::Block.new(..., conditional: true), Pin::Method.new(...) defaulting false), so there's only one thing to get right per site instead of two. It can't be a class-level constant: the bare Pin::CompoundStatement class is used both for an if's own condition (never conditional) and for then/else/rhs/rescue bodies (always conditional) - same class, different instances, different answers - so it stays an instance attribute, same as closure:/compound_statement: already are. lvasgn_node.rb's definite computation becomes a single-hop read: `!region.compound_statement.conditional`, no separate Region field. Pin::CompoundStatement#combine_with merges the new attribute via `choose`, since two versions of the same construct should already agree on it. Verified: full suite (1638 examples, 0 failures), typecheck self-check diffed clean against the prior baseline (587 problems, unchanged), rubocop clean on touched files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YbhZvdCv7xdziXyKJiPuGk
… when no literal one exists # Conflicts: # lib/solargraph/pin/parameter.rb # lib/solargraph/source/chain/call.rb
… the use site is dominated by it # Conflicts: # lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb
Per-key intersection dispatch worked on RBS >= 4.1 and silently did
nothing on 3.10.x/4.0.x: `Hash{"Index" => Float} & Hash{"Triggers" =>
Array<...>}` returned the union of both conjuncts' return types for
every `#fetch`, rather than narrowing to the conjunct whose key matched.
RBS's own core/hash.rbs changed how it declares the key parameter in
4.1.0. Before: `def fetch: (K arg0) -> V` (also `#[]`, `#dig`,
`#delete`). From 4.1.0: `def fetch: (_Key key) -> V`.
Pin::Signature#key_param_index only recognized the `_Key` shape, so on
older RBS it returned nil, Chain::Call#key_verified_conjuncts hit its
conservative "no verdict, don't narrow" branch, and every conjunct
passed through unfiltered.
key_param_index now takes the receiver's own resolved `key_types` tags
and falls back to them when no `_Key` parameter is found. Pre-4.1 the
key parameter is the class's own generic `K`, which has already been
resolved against the receiver by this point - for a literal-keyed
receiver that makes it the literal key type itself, directly comparable
to `key_types`. The `_Key` match is still tried first, so RBS >= 4.1
behavior is unchanged.
Symbol keys failed differently and are now covered by their own spec.
Symbols already infer as literals, so per-overload matching correctly
rejected the non-matching conjunct - but a pin whose overloads all fail
to match is not dropped, it falls through to its declared return type,
so the union survived anyway. Only key_verified_conjuncts can actually
remove a conjunct. That also produced three spurious "Wrong argument
type for Hash#fetch: arg0 expected :Index, received :Triggers" errors,
which this fixes.
Corrected both existing specs' pending reasons: they cited
castwide#1266, which is not involved. castwide#1266 addresses
nominal-vs-structural checking of `Hash::_Key`, and pre-4.1 RBS has no
interface at that position at all.
Verified against integration branch 2026-08-04 at c5f20ea (which has
castwide#1223 merged): the string- and symbol-key repros
report 0 problems on RBS 4.0.3 and 4.1.3, where 4.0.3 previously
reported the union plus, for symbols, generic<X> and the three argument
errors. spec/type_checker/levels/strong_spec.rb passes with 0 failures
on RBS 4.1.3 with both specs un-skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdEpnChUZyPznDWimtWmJL
…rameter # Conflicts: # spec/type_checker/levels/strong_spec.rb
Both specs pass now that castwide#1231 recognizes RBS < 4.1's `(K arg0)` key-parameter shape (merged here as bd9fb82). Verified on RBS 4.1.3 and 4.0.3: 93 examples, 0 failures. The `skip` markers claimed the specs were "flaky - fails or unexpectedly passes depending on run, not a stable per-Ruby/RBS-version split". That was a misreading of CI, not a real flake: - #49 run 1 (commit 82f464e) was recorded as failing every rspec matrix leg but one. In fact exactly one leg failed (`rspec (4.0, 4.0.3)`); the other twelve were `cancelled` by fail-fast after it, and `cancelled` was read as `failure`. - Run 2 (commit 92b6386) was recorded as an unexplained opposite result on the identical `rspec (4.0, 4.1.1)` leg. In fact that leg's only "failure" was two `FIXED` markers - the specs passed, but that run's pending guard was gated to Ruby 3.2, so a pass on Ruby 4.0 registered as an unexpected pass. Behavior was deterministic throughout, splitting purely on RBS version: < 4.1 failed, >= 4.1 passed. Also corrected the specs' comments, which blamed castwide#1266 - that PR addresses nominal-vs-structural checking of `Hash::_Key`, and pre-4.1 RBS has no interface at that position at all, so it was never involved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdEpnChUZyPznDWimtWmJL
The spec asserted the broken output verbatim on RBS < 4.1:
if Gem::Version.new(RBS::VERSION) >= Gem::Version.new('4.1.0')
expect(checker.problems.map(&:message)).to be_empty
else
expect(checker.problems.map(&:message))
.to eq(['Declared type Float does not match inferred type Float, generic<X> ...'])
end
castwide#1223's non-literal overload fallback (a1e8444)
then fixed the leak on pre-4.1 too, so the else branch started failing
because the bug was gone - a red that reads like a regression when it is
the opposite. Asserting known-broken behavior fails closed on
improvement; a pending marker would have reported FIXED instead.
Dropped the conditional, renamed the example to describe what now holds
rather than what used to break, and rewrote the comment as history.
This only goes stale where both castwide#1223 and
castwide#1231 are present, which today is this branch alone.
The spec came in with castwide#1231 and is absent from castwide#1223 and master; on
castwide#1231's own branch the leak is still real pre-4.1, so the conditional is
correct there and castwide's all-pre-4.1 matrix depends on it. This
change needs to travel to castwide#1231 only once castwide#1223 lands.
Verified: spec/type_checker/levels/strong_spec.rb, 93 examples, 0
failures, 4 pending on RBS 4.1.3, 4.0.3 and 3.10.0 - the three versions
in #49's matrix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdEpnChUZyPznDWimtWmJL
`parameters_from_inline_rbs` scanned the entire class body for
`#[...]`, so any bracketed comment in the body was read as the
superclass's type arguments. `# [:b, { c: :d }]` produced the
superclass name `Base<:b, { c: :d`, which resolves to nothing, and
every inherited method in the class then reported `Unresolved call` —
including on lines above the comment.
Match only where ruby/rbs's inline syntax puts it: directly after the
superclass, on the same line, `#[` with no space, with a closing `]`
required. `class Foo < Array # [String]` is now an ordinary comment.
Also removes a leftover debug `logger.warn` from
castwide#1173.
Fixes castwide#1300
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtwP1oKRXfJAbPQrPHm5oU
…op its superclass
ApiMap::Index#redefine_return_type set the pin's @return_type and then unconditionally iterated pin.signatures. Only Pin::Method defines #signatures, so an @!override naming a constant (e.g. URI::DEFAULT_PARSER) raised NoMethodError from inside map_overrides and aborted the entire catalog/typecheck run with a traceback into Solargraph internals, with nothing pointing back at the annotation. Guard the signatures loop with a Pin::Method check. The @return_type assignment above it already does the right thing for a constant: Pin::Constant#return_type is `@return_type ||= generate_complex_type`, and neither Pin::Base#reset_generated! nor Pin::BaseVariable#reset_generated! clears @return_type, so the override sticks. map_overrides also adds the tag to the pin's docstring beforehand, which generate_complex_type would pick up on its own. So @!override now works on constants rather than merely not crashing, and any other non-method pin reaching this path degrades to setting just the return type instead of aborting the run. Fixes castwide#1302 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
Gems commonly build classes with an anonymous-class assignment instead of
a `class` keyword, e.g. Asana's error hierarchy:
module Vendor
Specific = Class.new(StandardError) do
# @return [Integer]
def retry_after
5
end
end
end
`ParserGem::NodeProcessors::CasgnNode` mapped that `casgn` to an untyped
`Pin::Constant`, and the methods in the block landed on the enclosing
namespace, so `Vendor::Specific#retry_after` did not resolve:
`Unresolved call to retry_after on Vendor::Specific` at level strong.
Adds `Convention::ClassDefinition`, following the existing
`Convention::StructDefinition` / `Convention::DataDefinition` pattern: a
`casgn` node processor that recognizes `Class.new(...)` (with or without
a block), pushes a `Pin::Namespace` named after the constant, pushes a
`Pin::Reference::Superclass` when the argument is a constant, and
processes the block body with that namespace as the closure.
The processor is registered for `:casgn` after the Struct and Data
processors -- which keep winning for `Struct.new` / `Data.define` -- and
before `CasgnNode`, which still handles every other constant assignment
because the new processor returns true on non-match.
Fixes castwide#1303
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
A macro-generated `@!method` whose `@return` is a duck type resolves correctly: the generated pin carries `return_type.tag == "#quack"`, and the type resolves through ApiMap#get_complex_type_methods to a Pin::DuckMethod. This is easy to believe otherwise, because `solargraph pin` renders the pin via UniqueType#to_rbs and RBS has no duck-type syntax, so any duck type prints as `untyped` — identically for a plain method, a `@!method` directive, and a macro-generated one. Pin the working behavior so the macro path stays covered, alongside the existing class-name case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
The default-argument idiom - `tasks = ['a'] if tasks.nil?` followed by `tasks.each` - still reported `Unresolved call to each on Array<String>, nil`. PR castwide#1282 covered the dominance case (a use site inside the branch the reassignment dominates); here the use site is *after* the conditional, so what establishes the type on the path where the assignment did not run is the guard's condition, not dominance. At a merge point after an `if`, the incoming paths are (a) the clause ran and assigned a new value - already handled, that pin is unioned in - and (b) the clause did not run, leaving the original value, about which the condition tells us something. Path (b) was never asserted, so the original `Array<String>, nil` was unioned in unnarrowed. FlowSensitiveTyping#process_if now also asserts the opposite branch's condition facts over the rest of the enclosing compound statement, for the variables the clause definitely reassigns. Reusing #process_expression for that gets `&&`/`||`/`!` handling for free, including `and`'s deliberate refusal to propagate false-facts. The restriction to definitely-reassigned variables is what keeps this sound. Facts are filtered by variable name in #add_downcast_var, driven by a second FlowSensitiveTyping built over the same locals/ivars arrays with `restricted_names:` set. Without it, `xs = [] if xs.nil? || ys.nil?` would also narrow `ys` after the conditional, even though only `xs` was replaced. Likewise, only unconditional `lvasgn`/`ivasgn` in the clause count: an assignment nested in another conditional, or an `||=`, may leave the previous value in play. Guards that test something other than the variable (`tasks = ['a'] if flag`) and nil guards that don't reassign (`puts 'hi' if tasks.nil?`) keep nil in the type, as they must; specs cover both, plus the non-modifier `if`, `unless`, and else-clause forms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
The ignore added with the fix carried a one-off description. rules.rb keeps a tally of @sg-ignore texts grouped into buckets, so a novel string creates a bucket of one instead of joining an existing count. Reuse the established "Need to add nil check here" wording, matching this file's three sibling ignores on Range.from_node results. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
…faults Conflict resolution in flow_sensitive_typing.rb: - #initialize takes both this branch's `closure` positional arg and castwide#1282's `restricted_names:` keyword; the internal FlowSensitiveTyping.new in assert_after_guard now passes `closure` through. - attr_reader lists both :closure and :restricted_names. - Dropped castwide#1282's local always_leaves_compound_statement?. This branch already gets a richer version from Parser::NodeMethods that recurses into :begin for multi-statement clauses and treats raise/fail sends as leaving; a definition in the class shadows the included module, which broke the four "raise if()" nil-refinement specs. castwide#1282 wrote its simple copy before NodeMethods had one. Full suite: 1899 examples, 0 failures, 47 pending.
YARD has no handler for `Class.new`, so a gem that writes
module Asana
module Errors
RateLimitEnforced = Class.new(APIError) do
attr_accessor :retry_after_seconds
end
end
end
ships a yardoc containing one `ConstantObject` and no method objects at
all -- the block body survives only as the raw source string in
`ConstantObject#value`. `Mapper::ToConstant` ignores that string, so the
gem's pins held an untyped `Pin::Constant` and nothing else: no
superclass, no methods, and `ApiMap#get_method_stack` returned [].
`Mapper` now reparses that value. It builds `<name> = <value>`, checks
the parsed `casgn` node with the same
`Convention::ClassDefinition::ClassAssignmentNode.match?` predicate the
workspace path uses, and on a match maps a copy wrapped in the
constant's original module nesting -- which is what lets a superclass
written relative to that nesting (`APIError`, not
`Asana::Errors::APIError`) resolve through the same gates it had in the
gem. The resulting namespace, superclass reference and method pins are
emitted *instead of* the constant pin, so no two pins compete at that
path. Anything that fails to parse, or that does not produce a namespace
at the constant's path, falls back to the previous `ToConstant`
behavior.
Pins from that reparse cannot be emitted as-is. `NodeStripper` copies
each one, drops its parser nodes and any memoized YARD docstring, and
points it at the constant's location in the gem. Both matter: a retained
node makes `Pin::Method#probe` reach for `ApiMap#clip_at`, which raises
`FileNotFoundError` because the reparsed source has no cataloged source
map, and a retained node or docstring drags its parser buffer (or the
whole YARD registry) into the marshalled gem cache. Against the cached
asana-0.10.6 yardoc, the `Asana::Errors` pins marshal to 12,613 bytes
stripped versus 883,131 unstripped, against 6,142 bytes for the untyped
constants they replace.
The cost is that these pins no longer infer a return type from a method
body -- they keep only the YARD tags the gem wrote, which is all that
YARD-sourced pins ever had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
A modifier-if guard stopped being applied once the variable it guards
had been reassigned:
got = lookup(name)
return got.length if got # asserts got is nil/false below here
got = lookup(name)
got.length if got # Unresolved call to length on nil, Boolean
The first guard's `return` leaves the method, so FlowSensitiveTyping
asserts the false branch's facts - `got` is `nil, false` - over the rest
of the compound statement, and that downcast pin's presence runs to the
end of the method. The second `got = lookup(name)` overwrites the value
the fact was about, but ApiMap#var_at_location still combined the stale
pin in: Pin::BaseVariable#combine_with already let a definite
reassignment supersede the earlier pin's *assignments*, yet unioned
intersection_return_type and exclude_return_type unconditionally. The
`nil, false` intersection survived and intersected the new value down to
nothing.
Narrowing recorded against a value expires when that value is definitely
overwritten, so when #override_assignments? says `other` supersedes us,
keep only `other`'s intersection/exclude types instead of unioning ours
in.
#references_name? then blocked the supersede in the shape this was
actually observed in, `lib/solargraph/workspace/gemspecs.rb`:
specish = all_gemspecs_from_bundle.find { |specish| specish.name == name }
return to_gem_specification specish if specish
The self-reference exclusion exists so `x = x.foo` keeps the assignment
its own right-hand side resolves against, but a block parameter of the
same name shadows the outer variable for the whole block - the mention
inside the body is the parameter, not the variable being assigned. The
walk now descends only into a shadowing block's receiver, which is still
evaluated outside the block.
Two @sg-ignore comments in gemspecs.rb are no longer needed and are
removed. Facts stay in force up to the reassignment, and a reassignment
that only runs in a nested branch still does not supersede; specs cover
both, plus a guard on an unrelated variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
A codebase that documented one of these constants by hand still carries a `@!parse` stub for it, so the gem's new pins and the stub's pins now sit at the same path in different pinsets. Specs record what that produces. Resolution works: two `Pin::Namespace` pins (gem, then workspace), one superclass chain, and `get_method_stack` returns both method pins, so the call site type checks clean at strong and strict. Before this branch the same stub failed -- the gem's `Pin::Constant` sorted first and `get_method_stack` short-circuited on its undefined type. The stub no longer contributes its return tag, though. `Source::Chain::Call#resolve` infers from `stack.first`, which is the gem's untyped pin, so `@return [Integer]` written in the stub does not reach the call site -- inference is `undefined` with the stub and `undefined` without it. The stub is redundant rather than harmful, and `@!override <path>#<method>` with a `@return` tag retypes the gem pin directly, which is what a codebase wanting the type should use instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
A reassignment made inside a branch was ignored by a use site later in
that same branch:
def clean(items) # @PARAM items [Array<String>, nil]
if items.nil?
items = fetch_items
items.reject! { |i| i.empty? } # Unresolved call to reject! on nil
end
end
Pin::Parameter#typify prefers a reassignment's inferred type over the
declared @PARAM type only when the reassigning pin is `definite`, and an
assignment inside an `if` body is not definite - it may never run.
#override_assignments? already handles that distinction for a specific
position via #definite_reaches?: the use site falls inside the
CompoundStatement the assignment was made in, so on every path that
reaches it the assignment ran. But that verdict only reached
#combine_assignments; the combined pin still carried
`definite: definite || other.definite`, which was false on both sides,
so #typify fell back to the declared type and kept nil in the union.
The combined pin is built for one resolved location, so when the
supersede check passes there, the result is definite at that location.
ApiMap#var_at_location is the only caller that passes a location, so
locationless combines are unaffected: without one, #override_assignments?
already requires `other.definite`.
A reassignment nested in a further conditional, and a use site earlier in
the branch than the reassignment, both still keep the original type;
specs cover each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
The assignment-as-condition idiom asserted nothing about the variable it
assigns:
if (md = name.match(/\[(.*)\]/))
md[1].to_i # Unresolved call to []
else
0
end
Two things were missing. FlowSensitiveTyping#process_expression handled
:send, :and, :or and bare variable references, but not the one-child
:begin that parentheses produce, nor :lvasgn/:ivasgn - so the condition
was walked past without a fact being recorded. An assignment used as a
condition evaluates to the value assigned, so the branches say the same
thing about the variable as a bare reference would: not nil where the
condition held, `nil, false` where it did not.
Adding those handlers alone changed nothing, because IfNode#process ran
FlowSensitiveTyping *before* processing the condition node. The pin for
`md` is created by that condition, so #find_var had nothing to look up
and the facts were dropped. The FlowSensitiveTyping call now runs after
the condition is processed; the then/else clauses are still processed
after it, as before.
`if (md = ...) || fallback` stays unnarrowed without further work:
#process_or deliberately passes no true ranges down to its operands,
since either side alone may be what made the disjunction true. In the
else clause the variable is correctly narrowed to `nil, false` instead.
Four @sg-ignore comments in position.rb are no longer needed and are
removed.
WhileNode#process has the same FlowSensitiveTyping-before-condition
ordering, so `while (x = f.gets)` still misses this when `x` has no
earlier assignment; left alone here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
`NodeStripper` cleared a fixed list of instance variables -- `@node`, `@receiver`, `@assignments`, `@mass_assignment`. That list was complete when it was written and stopped being complete as soon as a pin grew another one: merging this branch with a base that carries `Pin::Method#compound_statement` left two `Parser::AST::Node`s alive on `#initialize`, reachable as `@compound_statement.@node` and `@compound_statement.@receiver`. Nothing named `@compound_statement` was in the list, so the pin holding those nodes was never copied or cleared. The stripper now walks every instance variable of every pin it copies and decides by what the value is: a parser node or a memoized YARD docstring is dropped, a pin is replaced by its stripped copy, an array of pins is mapped, and an array of nodes is emptied. A pin type or ivar added later is handled without this class knowing its name. `@compound_statement` in particular has to be copied rather than dropped, because `Pin::Base#closure` walks that chain when a pin has no directly assigned closure. Two coexistence specs asserted pin ordering that only holds on bases without `ApiMap::Store#combine_duplicate_method_pins`, which merges a gem pin and a `@!parse` stub pin at the same path into one `:combined` pin. They now assert what holds either way -- the method resolves, and the stub's `@return` tag is present in the stack -- with the difference described in a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
The integration branch renders a falsy-only receiver as `nil, false` where this branch renders `nil, Boolean`, so three exact-message assertions passed on each branch and failed on the merge. The property under test is that exactly one problem remains and its receiver is narrowed to the falsy types - not which of the two spellings the formatter picks - so match either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
…l assignment, assignment-in-condition # Conflicts: # lib/solargraph/parser/parser_gem/node_processors/if_node.rb # lib/solargraph/pin/base_variable.rb # lib/solargraph/workspace/gemspecs.rb
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
Restore tuple/literal element inference and track reassignment (#1196) castwide/solargraph#1223 — Restore tuple/literal element inference and track reassignment (Specious inference in flow-sensitive typing castwide/solargraph#1196)
Improve overload resolution and macro handling in Chain::Call castwide/solargraph#1247 — Improve overload resolution and macro handling in Chain::Call
Spec performance fixes castwide/solargraph#1237 — Spec performance fixes
Add intersection (A & B) types, including Hash-based record support castwide/solargraph#1231 — Add intersection (A & B), union (|), and grouping ([...]) type syntax
Rewrite PinCache as an instance-based engine, with wiring and CLI update castwide/solargraph#1252 — Rewrite PinCache as an instance-based engine, with wiring and CLI update
Fix raise/fail nil guards, root-scoped is_a?, case/when, and ||= narrowing in flow-sensitive typing castwide/solargraph#1259 — Fix raise/fail nil guards, root-scoped is_a?, case/when, and ||= narrowing in flow-sensitive typing
Backfill regression tests for previously-reverted/regressed behavior castwide/solargraph#1262 — Backfill regression tests for previously-reverted/regressed behavior
Fix Hash<K,V> tag round-trip crash and TypeChecker call-inference error boundary castwide/solargraph#1263 — Fix Hash<K,V> tag round-trip crash and TypeChecker call-inference error boundary
Narrow repeated calls to the same attr_reader-style accessor castwide/solargraph#1258 — Narrow repeated calls to the same attr_reader-style accessor
Narrow bare, implicit-self attr_reader-style accessor calls #53 — Narrow bare, implicit-self attr_reader-style accessor calls
Structurally verify RBS interface-typed expectations castwide/solargraph#1266 — Structurally verify RBS interface-typed expectations
Fix false positive: Struct.new(keyword_init: true) members are optional castwide/solargraph#1269 — Fix false positive: Struct.new(keyword_init: true) members are optional
Fix Chain#nullable? leaking nil from earlier &. into later calls castwide/solargraph#1271 — Fix Chain#nullable? leaking nil from earlier &. into later calls
Fix order-dependent generic resolution for same-class union receivers castwide/solargraph#1273 — Fix order-dependent generic resolution for same-class union receivers
Fix @generic return type lost when method also declares a block param castwide/solargraph#1274 — Fix @Generic return type lost when method also declares a block param
Allow arguments to satisfy RBS interface-typed parameters castwide/solargraph#1228 — Allow arguments to satisfy RBS interface-typed parameters
Give RBS bottom type its own tag instead of collapsing into undefined castwide/solargraph#1277 — Give RBS bottom type its own tag instead of collapsing into undefined
Fix ENV[] typechecking castwide/solargraph#1278 — Fix ENV[] typechecking (pp/RBS pin contradiction)
Resolve calls to a duck type param's own declared method castwide/solargraph#1280 — Resolve calls to a duck type param's own declared method
Expand RBS type aliases before conformance checks castwide/solargraph#1281 — Expand RBS type aliases before conformance checks
Update a parameter's flow-sensitive type after reassignment to a non-literal type castwide/solargraph#1282 — Update a parameter's flow-sensitive type after reassignment to a non-literal type
Fix return type inference for methods with an ensure clause castwide/solargraph#1285 — Fix return type inference for methods with an ensure clause
Fix and re-enable strong-level typechecking in CI castwide/solargraph#1240 — Fix and re-enable strong-level typechecking in CI
Fix generic binding through a cross-file @!parse stub castwide/solargraph#1288 — Fix generic binding through a cross-file @!parse stub
Fix @yieldparam type lost on multi-overload block-form methods castwide/solargraph#1290 — Fix @yieldparam type lost on multi-overload block-form methods
Fix duck_types_match? to check the inferred type's own duck interface castwide/solargraph#1295 — Fix duck_types_match? to check the inferred type's own duck interface
Match trailing keyword arguments to keyword/kwrest parameters, not by position castwide/solargraph#1292 — Match trailing keyword arguments to keyword/kwrest parameters, not by position
Narrow literal-equality (==/!=) guards against literal union members castwide/solargraph#1297 — Narrow literal-equality (==/!=) guards against literal union members
Fix Pin::Base#== missing presence, add regression coverage castwide/solargraph#1293 — Fix Pin::Base#== missing presence
Resolve generic type variables against union @param types castwide/solargraph#1299 — Resolve generic type variables against union @PARAM types
Don't let a bracketed comment in a class body drop its superclass castwide/solargraph#1301 — Don't let a bracketed comment in a class body drop its superclass