Skip to content

Fix unresolved method-level generic block parameters - #1326

Open
apiology wants to merge 4 commits into
castwide:masterfrom
apiology:fix-generic-block-param
Open

Fix unresolved method-level generic block parameters#1326
apiology wants to merge 4 commits into
castwide:masterfrom
apiology:fix-generic-block-param

Conversation

@apiology

Copy link
Copy Markdown
Contributor

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

Problem

Enumerable#each_with_object is typed in core RBS as:

[U] (obj U) { [U] (arg_0 Elem, obj U) -> untyped } -> U

U is a method-level generic bound by the obj argument passed to each_with_object. Inside the block, arg_0 (typed Elem, the receiver's own generic) resolves correctly, but obj (typed U) resolves to no type at all:

class Repro
  # @return [void]
  def scratch
    # @type [Array<Integer>]
    arr = [1, 2, 3]
    arr.each_with_object({}) { |e, memo| memo[e] = e }
  end
end

solargraph typecheck --level strong reports Unresolved call to []= on memo. The same shape of bug affects any method with a method-level generic bound via an argument passed alongside a block, e.g. Enumerable#inject.

Root cause

Pin::Callable#resolve_generics_from_context resolves a method-level generic from the method's own argument types into a shared resolved_generic_values hash, then recurses into the yielded block's parameter list to resolve that signature too. That recursive call is made with no per-parameter argument types available (nil), and the code took a shortcut for that case: it dup'd each block parameter instead of running it through resolve_generics_from_context. So even though resolved_generic_values["U"] was already populated from the method's own obj argument, the block's own obj parameter (typed generic<U>) never picked it up.

Fix

Always route block parameters through resolve_generics_from_context, passing arg_types&.[](i) instead of skipping resolution when arg_types is nil. ComplexType::UniqueType#resolve_generics_from_context already handles a nil context type by falling back to resolved_generic_values[type_param] || self, so this is a pure bug fix with no behavior change for the case where argument types are available.

Testing

Added spec cases in spec/source_map/clip_spec.rb covering each_with_object, inject, and a negative control (each_with_index, which has no method-level generic in its block signature and must be unaffected). Full suite: 0 failures.

each_with_object and inject-style methods with a method-level generic
bound via an argument passed alongside a block never resolved that
generic inside the block itself. resolve_generics_from_context dupped
block parameters instead of resolving them when no per-parameter
argument types were available, even though the generic had already
been bound one level up.
apiology added a commit to apiology/solargraph that referenced this pull request Aug 22, 2026
apiology added a commit to apiology/solargraph that referenced this pull request Aug 22, 2026
Comment thread lib/solargraph/pin/callable.rb Outdated
Cut the "even when... still route through" changelog framing
from the resolve_generics_from_context comment down to the
non-obvious constraint itself. Move the three new
method-level-generic block-parameter specs out of the very long
clip_spec.rb into a new spec/pin/callable_spec.rb, per review
comment.
Extract arg_types[i] into arg_type_for_param and shrink the
explanatory comment to one line, per review: prefer a well-named
local over narrating the expression in a multi-line comment.
apiology added a commit to apiology/solargraph that referenced this pull request Sep 5, 2026
Reorder so the comment states the action before the rationale, per
review.

Also replaces the ignore marker's reason. The previous wording blamed
uniq, but the block param is untyped because Array#flatten is declared
in core RBS as returning a bare Array: arr.flatten infers to ::Array
where arr.compact infers to ::Array<::String>. With no element type on
the receiver there is nothing for uniq's block param to bind to.

No open PR covers this. The nearest two address different shapes:
castwide#1326 is a method-level generic bound by an argument
alongside a block, and castwide#1274 is a generic return
type lost when the declaring method takes a block. Left as prose rather
than citing a URL that does not match.
@apiology
apiology marked this pull request as ready for review September 7, 2026 15:36
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