Skip to content

Resolve a self return type per union arm - #1314

Draft
apiology wants to merge 3 commits into
castwide:masterfrom
apiology:pr-a-rbs-self-union
Draft

Resolve a self return type per union arm#1314
apiology wants to merge 3 commits into
castwide:masterfrom
apiology:pr-a-rbs-self-union

Conversation

@apiology

@apiology apiology commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

A method that declares a self return type, called on a union receiver, resolves self against the whole union instead of the arm that supplied the method. Chain::Call#resolve split the binder into arms only to collect method pins, then flattened them and called #inferred_pins once with name_pin still bound to the whole union. Each arm's pin is now resolved against that arm alone.

class Counter
  # @return [self]
  def to_sym
    self
  end
end

# @param x [String, Counter]
# @return [Symbol, Counter]
def probe_union(x)
  x.to_sym
end
before:  probe.rb:10: Declared return type ::Symbol, ::Counter does not match
                      inferred type ::Symbol, ::String, ::Counter for #probe_union
after:   0 problems found.

This covers self in any position ��� @return [Array<self>] on an Alpha, Beta receiver now infers ::Array<::Alpha>, ::Array<::Beta>. Results dedupe on path and resolved return type, so a self-returning method shared by every arm still yields each arm: (String | Symbol)#itself stays String, Symbol.

It also removes an @sg-ignore here: Diagnostics::TypeCheck#diagnose calls level.to_sym where level is String, Symbol, which needed suppressing and now resolves to Symbol.

This PR was written by Claude Code on behalf of @apiology.

apiology and others added 3 commits August 17, 2026 16:12
A method whose RBS return type is `self` must resolve to the arm of a
union receiver that supplied the pin, not to the whole union.

    # @PARAM check_name [String, Symbol]
    # @return [Symbol]
    def to_sym_union(check_name)
      check_name.to_sym    # inferred ::Symbol, ::String
    end

`String#to_sym` is `-> ::Symbol` and `Symbol#to_sym` is `-> self`.
Chain::Call#resolve split the binder into arms only to collect method
pins, then flattened them and called #inferred_pins once with a name_pin
still bound to the whole union, so `self` expanded to `String, Symbol`.

Resolve each arm's pins against that arm, then dedup on both path and
resolved return type so a shared self-returning pin (e.g. Kernel#itself)
still contributes every arm.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
The per-arm resolution added in the previous commit is not RBS-specific:
`self_to_type` runs on the pin's parsed `ComplexType`, so a YARD
`@return [self]` tag reaches the same code as an RBS `-> self`. On a
pure-YARD file with no RBS involved, `solargraph typecheck --level
strong` reports on master:

    Declared return type ::Symbol, ::Beta does not match inferred type
    ::Symbol, ::Alpha, ::Beta for #probe_union

and reports no problems here.

Add two specs pinning that path: `@return [self]` on one arm of a
two-class union, and `@return [Array<self>]` shared by both arms, where
`self` inside a generic distributes to `Array<Alpha>, Array<Beta>`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
`Diagnostics::TypeCheck#diagnose` computes `level` as `String, Symbol`
(`args.reverse.find { ... } || :normal`) and passes `level.to_sym` to
`TypeChecker.new`. `String#to_sym` is `-> ::Symbol` and `Symbol#to_sym`
is `-> self`, so while `self` resolved against the whole union the call
inferred `Symbol, String` and the result needed suppressing.

Resolving each arm's pin against that arm alone narrows it to `Symbol`.
The comment now suppresses nothing, and `--level strong` reports it as
an unneeded `@sg-ignore`. Removing it brings this branch to 529
problems in 90 of 250 files, matching master at 8fda633.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
@apiology apiology changed the title Resolve an RBS self return type per union arm Resolve a self return type per union arm Aug 18, 2026
@apiology

Copy link
Copy Markdown
Contributor Author

Yes ��� YARD reaches this too, and the fix covers it without a second change.

self_to_type runs on the pin's parsed ComplexType, downstream of whichever syntax produced the declaration, so @return [self] and an RBS -> self arrive at the same call in #inferred_pins. Measured rather than reasoned about ��� this file has no RBS in it at all:

class Alpha
  # @return [Symbol]
  def to_thing; end
end

class Beta
  # @return [self]
  def to_thing; end
end

# @param x [Alpha, Beta]
# @return [Symbol, Beta]
def probe_union(x)
  x.to_thing
end

solargraph typecheck --level strong, master at 8fda63384:

probe.rb:17: Declared return type ::Symbol, ::Beta does not match inferred type ::Symbol, ::Alpha, ::Beta for #probe_union
1 problem found.

Same file at 4931a9554: 0 problems found.

Two further shapes, both pure YARD, inferred type before and after:

shape master branch
@return [self] inherited by both arms ::Alpha, ::Beta ::Alpha, ::Beta
@return [Array<self>] inherited by both arms ::Array<::Alpha, ::Beta> ::Array<::Alpha>, ::Array<::Beta>

The first is the dedup guard holding ��� a method shared by every arm is not over-split. The second is the generic case distributing.

This comment was written by Claude Code on behalf of @apiology.

apiology added a commit to apiology/solargraph that referenced this pull request Aug 18, 2026
One conflict in diagnostics/type_check.rb: the PR predates the workspace
keyword this branch passes to TypeChecker.new, and its side would have
dropped it. Kept this branch call.
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