Skip to content

Read implicitly-returns-nil per RBS overload - #1316

Draft
apiology wants to merge 1 commit into
castwide:masterfrom
apiology:fix-implicit-nil-per-overload
Draft

Read implicitly-returns-nil per RBS overload#1316
apiology wants to merge 1 commit into
castwide:masterfrom
apiology:fix-implicit-nil-per-overload

Conversation

@apiology

Copy link
Copy Markdown
Contributor

Array#max(1) comes back with a spurious , nil appended, so this fails at --level strong:

# @return [Array<String>]
def probe
  a = ['x', 'y']
  a.max(1)
end

# probe.rb:2: Declared return type ::Array<::String> does not match
#             inferred type ::Array, nil for #probe

RBS declares no nil there. %a{implicitly-returns-nil} is attached to an individual overload, and rbs 4.1.3's core/array.rbs puts it only on the zero-argument ones:

def max: %a{implicitly-returns-nil} () -> E
       | %a{implicitly-returns-nil} () { (E a, E b) -> _CompareToZero } -> E
       | %a{implicitly-returns-nil} %a{warning: ...} () { (E a, E b) -> _CompareToZero? } -> E
       | (::int count) -> ::Array[E]
       | (::int count) { (E a, E b) -> _CompareToZero } -> ::Array[E]
       | %a{warning: ...} (::int count) { (E a, E b) -> _CompareToZero? } -> ::Array[E]

The annotation is also no longer passed down to block return types: it describes what the method returns, not what the block returns, and a block that may return nil says so with ? ��� as the third and sixth overloads above do.

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

RBS attaches %a{implicitly-returns-nil} to an individual overload.
method_def_to_sigs read it from decl.overloads.first and then applied that
one boolean to every overload of the method and to every block return type.

For Array#max, rbs 4.1.3 annotates only the zero-argument overloads:

  def max: %a{implicitly-returns-nil} () -> E
         | %a{implicitly-returns-nil} () { (E a, E b) -> _CompareToZero } -> E
         | %a{implicitly-returns-nil} %a{warning: ...} ()
             { (E a, E b) -> _CompareToZero? } -> E
         | (::int count) -> ::Array[E]
         | (::int count) { (E a, E b) -> _CompareToZero } -> ::Array[E]
         | %a{warning: ...} (::int count)
             { (E a, E b) -> _CompareToZero? } -> ::Array[E]

Solargraph produced ::Array[E], nil for the (count) overloads, so this
failed --level strong:

  # @return [Array<String>]
  def probe
    a = ['x', 'y']
    a.max(1)
  end

  probe.rb:2: Declared return type ::Array<::String> does not match
  inferred type ::Array, nil for #probe

The block return type is a separate matter: the annotation describes what
the method returns when it finds no value, not what the block itself may
return. A block whose return type is nullable says so in RBS with `?`, as
the third and sixth Array#max overloads do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
apiology added a commit to apiology/solargraph that referenced this pull request Aug 18, 2026
One conflict in spec/rbs_map/conversions_spec.rb, where both sides append
contexts at the same point and git interleaved them into three hunks that
cut across each other block boundaries. Reconstructed each side whole - the
four type-alias contexts from this branch, then the PR
 implicitly-returns-nil context - rather than resolving hunk by hunk.
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