Stop flagging unreachable code as errors - #56
Merged
apiology merged 17 commits intoSep 9, 2026
Conversation
Pin::Base#== compares #location but not #presence. combine_with results choose the earliest assignment's #location, so two combined pins covering a different number of assignments to the same variable can share #location while covering different #presence ranges - e.g. one pin combined through a variable's first reassignment, another combined through its second. Any caller keying off of #== (e.g. Array#include?) treated these as the same pin. BaseVariable#== now also compares presence, intersection_return_type, and exclude_return_type. Adds two regression specs: - spec/pin/base_variable_spec.rb: directly exercises the equality gap above - fails without the fix, passes with it. - spec/type_checker/levels/strong_spec.rb: a 13-line repro (castwide#1288 (comment)) where this equality gap, combined with in-flight flow-sensitive-typing work (castwide#1258, castwide#1282), produces a false "Unresolved call" via Chain's inference recursion guard. Not currently reachable on master alone (verified neither castwide#1258 nor castwide#1282 reproduces it in isolation, only the two combined) - kept as a standing guard so whatever future combination reintroduces the failure mode gets caught regardless of merge order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YT7qJXsRVt8W7ULFmwjvLj
BaseVariable#== now compares presence (97127a9), so pins produced by independent flow-sensitive-typing facts at the same location (e.g. the two operands of x.nil? || x.is_a?(Foo)) no longer get deduplicated via Array#include? before being combined. Combining them unions their exclude_return_type sets, which can end up excluding every member of the declared type - collapsing ComplexType#exclude's result to undefined even though the type was well-defined before exclusion. That surfaced as new self-hosted strong-typecheck failures on lib/solargraph/pin/base.rb ("Unresolved call to inspect") reported at castwide#1293 (comment), introduced by 97127a9 without touching that file at all. ComplexType#exclude now treats an exclusion built from more than one excluded type as a no-op when it would otherwise remove every possible type, since that combination reflects contradictory flow facts (unreachable/defensive code) rather than a real type error. A single-source exhaustive exclusion (e.g. a variable directly assigned nil despite a non-nilable declared type) still collapses to undefined as before. Verified against the parent commit (8fda633): whole-project solargraph typecheck --level strong problem count drops from 533 to 531, an exact match after removing the two new false positives with no other diff. Full test suite: 1626 examples, 0 failures.
@todo comment only - explains that once castwide#1277 (RBS bottom type) lands, ComplexType#exclude could tag `bot` for any exhausted exclusion instead of just the multi-source case handled here, making this branch's special-casing unnecessary. The two PRs are otherwise independent: this touches only #exclude, castwide#1277 touches #qualify and elsewhere in complex_type.rb, so they merge cleanly in either order with no coordination needed.
ComplexType#exclude previously only collapsed to `undefined` for single-item exclusions (e.g. `!x.is_a?(Foo)` narrowing a declared Foo to nothing), leaving the multi-item case as a same-type no-op. Now that castwide#1277 gives RBS's bottom type its own tag, both cases can collapse to `bot` instead - it correctly signals "this code is unreachable" rather than "this type is unknown." That surfaces a second gap: Call#resolve had no path for a bot-typed receiver, so any method call chained onto one came back "Unresolved call to <method> on bot" - a false positive, since bot is a subtype of everything and the call is unreachable code anyway. Adds a bot? branch that returns a DuckMethod pin (return type bot) so downstream resolution has a real Pin::Method to work with while bot keeps propagating. Fail-first verified: reverting just the Call#resolve change while keeping the exclude change reproduces "Unresolved call to length on bot"; with both, the new strict-level spec passes.
The integration-branch merge of this commit surfaced (via Solargraph/strong, which runs with SOLARGRAPH_ASSERTS=on) "Closure not set on Solargraph::Pin::DuckMethod ... from :chain": the bot? DuckMethod pin never passed closure:, which is harmless until something downstream reads Pin::Base#closure, which asserts under strict mode when unset. Passes name_pin.closure through, matching the fix already applied to the integration branch's equivalent method_stack_pins code path.
… fix-pin-equality-missing-presence
3 tasks
apiology
added a commit
that referenced
this pull request
Aug 18, 2026
Clean auto-merge. Its branch already carried castwide#1277, merged here first, so nothing of castwide#1277 arrives twice.
The four `@sg-ignore Should add type check on other` comments in `BaseVariable#==` existed because `other` is declared `Object`, which has no `assignment`, `presence`, `intersection_return_type`, or `exclude_return_type`. With solargraph-rails loaded, that plugin declares ActiveSupport's `Object#presence` as `() -> self?`. That unrelated same-named method satisfied `other.presence`, so the suppression on that line stopped suppressing anything and CI's plugin-loaded jobs reported it as `Unneeded @sg-ignore comment`, while the other three still suppressed real findings without the plugin. No single set of suppressions could satisfy both configurations while `other` stayed `Object`. The guard is the fix the suppression text named. It is behaviorally equivalent to what was there: `super` is `Pin::Base#==`, which starts with `nearly?`, which starts with `instance_of?(other.class)`, so a non-`BaseVariable` `other` already returned false. Verified with `solargraph typecheck --level strong` both with and without solargraph-rails configured: base_variable.rb reports zero findings from this method in both, where the plugin configuration previously reported one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AsvDi68YqsKoBtS2kg9ch
Several comments in this branch ran 6-14 lines explaining a single mechanism, well past this repo's 1-3-line budget. Compress them to state the same facts in fewer words, drop a stale upstream PR reference (castwide#1282, closed in favor of castwide#1338) from a spec comment, and remove a spec-level comment that duplicated the adjacent source comment word for word. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJxUeueBgXj2qUqrsaa7yY
The prior compression pass still left a sentence describing which upstream PRs were needed and when this bug was verified reproducible -- history about how the test came to exist, not anything true about the code today. The it description already states the scenario; the bare link is enough for anyone who wants the original discussion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJxUeueBgXj2qUqrsaa7yY
Checked every plausible consumer -- the chain.rb recursion guard, combine_with's own merge logic, var_at_location, and this PR's full 155-example type-checker spec suite with only this fix reverted -- and none of them depend on it. The old equality is a real inconsistency (two pins with different presence compare equal) but currently latent, not something actively producing a wrong result anywhere in this codebase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJxUeueBgXj2qUqrsaa7yY
complex_type.rb's exclude comment: 3 lines to 2. call.rb's DuckMethod comment: 6 lines to 4, closer to this repo's 1-3-line budget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJxUeueBgXj2qUqrsaa7yY
Both suppressed a typecheck complaint about a bare `raise` -- exactly the class of problem this PR's bot-tagging fix addresses. Verified by removing each flagged line and re-running the full self-typecheck: no new problem appears at either line, and the project's total count now exactly matches the base branch (590 problems in 96 files). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJxUeueBgXj2qUqrsaa7yY
The it description already states the scenario in full; the bare URL added nothing the title didn't already say.
Owner
Author
|
Claude: Pushed directly onto |
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.
This PR was written by Claude Code on behalf of @apiology.
Base: castwide/solargraph#1277, not yet merged.
Problem: A defensive "none of the expected types" guard still gets flagged as calling an unresolved method — even a method every object has.
Every project that writes this common defensive-guard shape hits a spurious error on correct code instead of a clean typecheck.
Solution: Excluding every member of a declared type now tags the unreachable branch
bot(from castwide/solargraph#1277) instead ofundefined, so calls on it resolve through a vacuously-valid pin instead of erroring.