Skip to content

Stop flagging unreachable code as errors - #56

Merged
apiology merged 17 commits into
fix-raise-abort-bot-typefrom
fix-pin-equality-missing-presence
Sep 9, 2026
Merged

Stop flagging unreachable code as errors#56
apiology merged 17 commits into
fix-raise-abort-bot-typefrom
fix-pin-equality-missing-presence

Conversation

@apiology

@apiology apiology commented Aug 13, 2026

Copy link
Copy Markdown
Owner

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.

class Ok; end
class Err; end

# @param status [Ok, Err]
def handle(status)
  return unless !status.is_a?(Ok) && !status.is_a?(Err)

  raise "impossible status: #{status.inspect}"
end
# solargraph typecheck: Unresolved call to inspect

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 of undefined, so calls on it resolve through a vacuously-valid pin instead of erroring.

apiology and others added 7 commits August 12, 2026 18:35
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.
@apiology apiology changed the title Merge #1277 (bot type) prerequisite into #1293 (pin equality) Fix Pin::Base#== missing presence, add regression coverage Aug 18, 2026
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
@apiology apiology changed the title Fix Pin::Base#== missing presence, add regression coverage Stop losing flow-typing facts, and stop flagging unreachable code as errors Aug 21, 2026
apiology and others added 5 commits August 31, 2026 21:45
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
@apiology apiology changed the title Stop losing flow-typing facts, and stop flagging unreachable code as errors Stop flagging unreachable code as errors Sep 8, 2026
apiology and others added 3 commits September 8, 2026 14:37
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.
@apiology
apiology merged commit bc21a49 into fix-raise-abort-bot-type Sep 9, 2026
28 checks passed
@apiology

apiology commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Claude: Pushed directly onto fix-raise-abort-bot-type, which is castwide#1277's head branch — that PR now includes this PR's commits (the bot-tagging exhaustive-exclusion fix). GitHub picked this up automatically as merged once the base branch absorbed the commits.

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