Skip to content

Prefer a block signature that declares parameters - #61

Draft
apiology wants to merge 4 commits into
fix-1286-generic-cross-file-parsefrom
fix-combined-block-signature
Draft

Prefer a block signature that declares parameters#61
apiology wants to merge 4 commits into
fix-1286-generic-cross-file-parsefrom
fix-combined-block-signature

Conversation

@apiology

Copy link
Copy Markdown
Owner

A block parameter resolves as undefined when a gem's yardoc and a workspace
@!parse stub both declare a method and only the stub declares the yield:

# in the gem
module Widgetbox
  class << self
    # @return [String]
    def build(&block) = 'x'
  end
end

# in the workspace
# @!parse
#   module Widgetbox
#     class << self
#       # @yieldparam config [String]
#       # @return [String]
#       def build(&block); end
#     end
#   end

Widgetbox.build { |config| config.upcase } # Unresolved call to upcase

ApiMap::Store combines the pins into one carrying two signatures. The gem's
bare &block yields { () -> }, and Chain::Call took the first, never
reading the stub's @yieldparam.

Signatures whose block declares parameters now sort ahead. That changes the
outcome only where both already matched the call's arity and arguments and the
winner was arbitrary.

Two adjacent shapes remain uncovered: a stub without @return, whose
signatures combine_signatures discards as all-undefined, and a gem using
def self.build against a stub using class << self, where the pins never
combine.

Based on castwide#1288, whose store combining produces the two-signature pin. On
master the @!parse pin is discarded by [stack.first].compact before
selection runs, so the two-signature pin does not exist and this sort has
nothing to reorder — the symptom is the same there, by a different mechanism.

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

apiology and others added 2 commits August 19, 2026 16:21
When a method has more than one block-carrying signature, Chain::Call
selected the first one in source order. A signature built from a bare
`&block` parameter yields `{ () -> }` - it says the method takes a block
but nothing about what the block receives - so when it sorted ahead of a
sibling signature carrying @yieldparam types, the block's parameters
resolved as undefined.

This shows up wherever a gem's own yardoc and a workspace `@!parse` stub
both declare the same method: ApiMap::Store combines them into one pin
with two signatures, the gem's `&block` signature first, and the
annotation's @yieldparam types were never consulted.

Within the block-carrying group, signatures whose block declares
parameters are now tried before those whose block declares none. This
only changes which signature wins where both already match the call's
arity and argument types, and the previous winner was whichever happened
to be first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AsvDi68YqsKoBtS2kg9ch
Chain::Call tries a method pin's signatures in preference order and
stops as soon as one produces a defined return type. When none does -
every signature returning untyped, as RBS `untyped` maps to
ComplexType::UNDEFINED - the loop ran to the end and the last signature
to match supplied the pin passed to `with_single_signature`, discarding
the better earlier match.

A blockless signature matches a call that passes a block, since Ruby
accepts a block for any method. So where a gem's bare `def build; end`
combines with a workspace `@!parse` stub declaring @yieldparam types,
the blockless signature was tried second and won, and the block's
parameters resolved as undefined.

A later signature now replaces an earlier match only when it produces a
return type. This is complementary to the preference ordering added in
the previous commit: that orders the block-carrying group internally,
which is a no-op when the group holds a single signature, as it does
here.

The full spec suite is unchanged (1631 -> 1632 examples with the new
spec, 0 failures) and `solargraph typecheck --level strong` over this
codebase reports the same 577 problems, so nothing here relied on a
later overload overwriting an earlier one under an undefined-return
tie. The added lines are kept tight to stay under the existing
Metrics/BlockLength limit for this block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AsvDi68YqsKoBtS2kg9ch
apiology added a commit that referenced this pull request Aug 22, 2026
…-08-04

# Conflicts:
#	lib/solargraph/source/chain/call.rb
Chain::Call#inferred_pins tried block-taking overload signatures
before no-block ones unconditionally, on the assumption that
Callable#arity_matches? would reject a block-taking overload when
the call site passed no block. That assumption held until commit
922d073 (castwide#1274) made block_required? default
false for YARD-derived signatures, since YARD has no syntax to mark
a block mandatory. A YARD @!override with two @overload tags
differing only by block presence now resolves a no-block call site
to the block overload return type (often an unresolved generic)
regardless of declaration order.

Fix: only try block-taking overloads first when the call site
actually passed a block; try no-block overloads first otherwise.
Regression spec added to strong_spec.rb.

Extracted the overload-ordering logic into a new dispatch_order
helper so inferred_pins stays under the Metrics/AbcSize,
Metrics/PerceivedComplexity, and Metrics/BlockLength thresholds
the added branching pushed it over.
Call#inferred_pins prefers a block-taking overload signature that
declares yielded parameters over a sibling that yields { () -> },
and keeps the first matching signature unless a later match
supplies the return type the first one could not. Both existed
only as clip_spec cases exercised through combined gem/annotation
pins; add direct call_spec coverage for the dispatch order and the
selected_signature_pin replacement rule.
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