Skip to content

Rebind self in Class.new blocks; nested blocks inherit rebound binders - #1310

Open
apiology wants to merge 13 commits into
castwide:masterfrom
apiology:classnew-yieldreceiver
Open

apiology wants to merge 13 commits into
castwide:masterfrom
apiology:classnew-yieldreceiver

Conversation

@apiology

@apiology apiology commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Solargraph's strong typecheck reports Unresolved call for methods that work at runtime inside a Class.new block:

Class.new do
  define_method(:foo) { nil }                     # Unresolved call to define_method
  %i[a b].each { |m| define_method(m) { nil } }   # same, one block deeper
end

Two changes:

  1. Core fill: Class#new gets @yieldreceiver [::Class]. RBS binds only Class#initialize ([self: Class], rbs >= 4.1); Class#new is (*untyped, **untyped) -> untyped in every version, so a [self: ...] translator fix never reaches this call site. @yieldreceiver can't express the superclass parameter, so the fill stays unparameterized.
  2. Pin::Block#rebind/#binder cascade: a block with no rebind of its own inherits its enclosing block's binder. Without it, every existing fill (class_eval, ...) stops one nesting level down.

Three new strong-level specs (direct, .each-nested, class_eval-nested).

🤖 This PR was written by Claude (Claude Code), operated by @apiology.

Two changes so strong typecheck resolves define_method (and any other
Module method) inside Class.new blocks:

1. Core fill: Class#new gets @yieldreceiver [::Class]. RBS records the
   binding only on Class#initialize ([self: Class], rbs >= 4.1);
   Class#new, the method actually resolved for Class.new-with-a-block,
   is (*untyped, **untyped) -> untyped in every RBS version, so a
   translator fix for [self: ...] would not reach this call site.

2. Pin::Block#rebind/#binder cascade: a block with no rebind of its own
   inherits its enclosing block's rebound binder instead of falling back
   to its statically-parsed context. Matches Ruby semantics (a block
   does not change self) and makes the existing class_eval/instance_eval
   fills work below one level of block nesting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
apiology added a commit to apiology/solargraph that referenced this pull request Aug 17, 2026
…er inheritance

# Conflicts:
#	spec/type_checker/levels/strong_spec.rb
apiology and others added 4 commits August 17, 2026 10:23
A receiverless call is resolved only against the binder's namespace, so
top-level helpers went unresolved wherever the binder was not itself the
root namespace - from an ordinary instance method, and (once a block
inherits an enclosing rebound binder) inside rebound blocks:

  Object.define_method(:foo) do
    some_top_level_helper   # Unresolved call to some_top_level_helper
  end

Top-level defs are pinned in the root namespace, not under Object, so no
ancestry walk reaches them. At runtime they are private instance methods
of Object and a receiverless call reaches them whatever self is, verified
against Ruby. Looking up '' reuses the root-context lookup
ApiMap#get_methods already implements for that namespace, so this adds no
new resolution rule.

Restricted to head? - no explicit receiver - because these methods are
private: 'foo'.top_level_helper raises NoMethodError, and that stays
unresolved. Foo.class_eval { bar } with bar an instance method of Foo
also stays unresolved, matching Ruby's NameError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Positive: an ordinary instance method body and a class_eval block body
both reach a top-level def, and the binder and a local variable each win
over a top-level method of the same name.

Negative: a top-level def is a private instance method of Object at
runtime, so 'str'.helper raises NoMethodError and must stay unresolved.

Verified locally that the positives fail with the top_level_pins fallback
removed, and the negative fails if root-namespace methods are merged into
Object's instance methods in ApiMap instead of guarding on head?.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Conversions#calculate_method_visibility received the scope it was
computing a pin for but ignored it when handling a module function,
marking both the instance and the singleton copy private. At runtime
module_function makes only the instance copy private; the singleton
copy is public, and is how these are normally called.

The effect was that Marshal.dump, Signal.trap, FileUtils.rm_rf and
JSON.parse all resolved to private pins, so any caller that asked for
public methods - completion behind an explicit receiver, for one -
could not see them.

The YARD mapper already applied this rule; see
YardMap::Mapper::ToMethod.make, which splits module_function into a
private instance pin and a public class pin. This brings the RBS
converter in line with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Replaces the head?-guarded top_level_pins fallback in Chain::Call with
two changes that together express Ruby's actual rule.

ApiMap#inner_get_methods now merges the root namespace's instance
methods into Object's, but only when the request admits :private -
the same pattern already used one method down for Kernel. Methods
defined at the top level are private instance methods of Object at
runtime, so this is where they belong.

Chain::Call#resolve now asks for a visibility that depends on the call
site instead of always taking get_method_stack's private-inclusive
default. A receiverless call (head?) reaches private and protected
methods; a call with an explicit receiver reaches them only from
inside the receiver's namespace or a subclass, mirroring
ApiMap#get_complex_type_methods.

That keeps 'str'.top_level_helper unresolved without a special case in
the chain, and also stops explicit-receiver calls from resolving
unrelated classes' private methods.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Comment thread lib/solargraph/pin/block.rb
Names what the two lines actually do (force an enclosing block own
@Rebind memo to populate before binder/context fall through to it)
instead of leaving them inline under a comment.
"Memo" was unexplained jargon. State plainly that it forces the
enclosing block's cached rebind to compute first, since this block's
own binder/rebind fall through to it when unset.
Parallels the sibling maybe_rebind: both are conditional actions
guarded on a runtime check, not just cache bookkeeping. The docstring
now carries the why (binder/context fall through to the closure when
unset) instead of the name restating the mechanism.
apiology added a commit to apiology/solargraph that referenced this pull request Aug 27, 2026
Top-level method resolution in receiverless calls (including from
inside Class.new-block/define_method contexts with a rebound self),
gated on call-site visibility via a new visibility_for helper.

Resolved a conflict in lib/solargraph/source/chain/call.rb: the
incoming branch predates this repo's intersection-aware
method_stack_pins/method_pins_for_binder refactor. Ported
visibility_for through that newer structure instead of restoring the
old inline dispatch loop - threaded name_pin through both helpers so
the non-intersection get_method_stack call can compute visibility the
same way the top-level pin_groups loop already did.
apiology added a commit to apiology/solargraph that referenced this pull request Aug 27, 2026
ComplexType#intersect_with calls intersection_type.select when no
type conforms. ComplexType defines #select; UniqueType does not, so
a bare UniqueType (a value the method's own signature allows) fell
through to Kernel#select, which is private, raising NoMethodError.

castwide#1310's visibility_for fix stopped that call from
silently no-oping and turned it into a real error.

Normalize with #to_a (defined on both classes) before calling
#select, matching the existing ComplexType-or-UniqueType idiom used
elsewhere in this codebase.
apiology and others added 3 commits August 28, 2026 18:56
# Conflicts:
#	lib/solargraph/source/chain/call.rb
The Kernel-specific fallback in calculate_method_visibility checked
Kernel.private_method_defined? without checking which scope it was
computing a pin for, so it marked the public singleton copy of a
Kernel module function private too - the same bug the module_function
check right below it already guards against with scope == :instance.

Kernel.loop and other Kernel module functions called with an explicit
receiver stopped resolving once Chain::Call#resolve began filtering
by visibility for explicit-receiver calls, surfacing a pin visibility
that was already wrong but previously went unfiltered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Evcn8ABJk8ZhNmiDxJ28Th
The Kernel-specific fallback in ToMethod.make checked
Kernel.private_method_defined? without checking which scope it
was computing a pin for, marking the public singleton copy of a
Kernel module function private too - the same class of bug
9a309f4 already fixed on the RBS-conversion side
(rbs_map/conversions.rb), guarded there by scope == :instance the
same way the module_function? check right below this one already
guards on this file.

Confirmed directly: building pins for Kernel.loop via
YardMap::Mapper::ToMethod.make returned :private for both the
instance and singleton copies before this change; only the
singleton copy should be private, matching real Ruby's own
visibility (Kernel#loop is private, Kernel.loop is public).

Added a regression spec mirroring the RBS-side one, covering both
the instance and singleton copies directly against ToMethod.make.
apiology added a commit to apiology/solargraph that referenced this pull request Aug 29, 2026
Includes the Kernel module-function visibility fix on both the RBS
(conversions.rb) and YARD (to_method.rb) pin-generation paths -- a
public singleton copy of a Kernel module function was being marked
private too, because the private-method fallback check did not
guard on scope the way the module_function check right below it
already did.

Conflict in chain/call.rb: kept HEAD's side in all three hunks --
this branch's own visibility_for/union-arm-resolution logic (from
the earlier castwide#1310 merge plus later castwide#1231/castwide#1314 work) already
supersedes what this incoming diff carries for that file.
Cut the visibility_for docstring to 2 lines and the top-level
private-method comment in inner_get_methods to 1 line, per PR review.
apiology added a commit to apiology/solargraph that referenced this pull request Sep 1, 2026
Two comments this PR added ran over the 1-3 line budget or narrated
an external reference instead of leaving it bare.

- api_map.rb: compressed the 5-line explanation of why Class#new's
  return type is widened to Object down to 2 lines, keeping only the
  RBS constraint and the resulting behavior.
- strong_spec.rb: reworded the define_method-is-unresolved comment
  and replaced the narrated "castwide#1310's territory" reference with the
  bare PR URL (castwide#1310).
apiology added a commit to apiology/solargraph that referenced this pull request Sep 5, 2026
Resolve every conflict by where `ours` came from, not by comparing the
two sides on their merits. Overcommit 0.71.0 destroyed merge state on
this branch repeatedly, so several deliberate castwide#1231 changes present as
divergent local work.

Where `ours` is castwide#1231 code that castwide#1231 itself later changed, take theirs:
the `closure` parameter (derivable as `name_pin.closure`), the inlined
`parse_type_string`, `Signature#key_param_index`, the four
`*_complex_type` object-graph builders, and `key_verified_conjuncts`,
now `argument_verified_conjuncts`.

Where `ours` is independent work, port it onto castwide#1231's structure and
signatures rather than the reverse: `receiver_path`/`narrowed_call_pin`,
per-arm `self_binder` resolution, keyword-argument matching and
`require_literal`, the yielded-parameter signature preference, the
duck-type and bot branches, castwide#1310's `visibility_for`, and castwide#1255's RBS
type-alias expansion, which now threads through `type_to_tag`.

Auto-merge silently dropped `Intersection#intersection_tag` and
`ComplexType::QUOTE_CHARACTERS`; both are restored. castwide#1231's
`RBS_INTERFACE_TO_GENERIC` stub stays out - it stands in for structural
interface types, which castwide#1266 supplies here for real.
Its `pull/1223` pending guards go too, since castwide#1223 is merged on this
branch.
@apiology
apiology marked this pull request as ready for review September 5, 2026 22:27
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