Map Forwardable delegations to method pins - #1311
Draft
apiology wants to merge 2 commits into
Draft
Conversation
def_delegator and def_delegators define real methods whose behavior comes from the method they forward to, but Solargraph mapped neither, so every call to a delegated method was unresolved and any method returning one could not infer its return type. SendNode now maps both into Pin::DelegatedMethod - the pin class added in 602 and until now only constructed by external plugins - which resolves the receiver chain and the forwarded method lazily, so the delegated method reports the forwarded method's parameters, return type and signatures. Instance-variable, class-variable and method receivers are supported, as is def_delegator's optional alias argument. Expansion only fires in a namespace that extends Forwardable; SingleForwardable's class-scope delegation is not covered. Method pins synthesized from a class-body statement cover that statement's range but have no body of their own, so treating them as closures makes the declaring statement resolve in instance scope. _locate_pin already skipped attribute pins for this reason; aliases and delegations need the same treatment. Without it, `alias_method :a, :b` in a class body reports "Unresolved call to alias_method" at strong level on master today, and def_delegators would have reported the same. Specs catalog a Bench with external_requires so Forwardable's own pins are loaded, which TypeChecker.load_string does not do; that lets each delegation spec assert an empty problem list. Full suite: 1541 examples, 0 failures, 59 pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT SKIP=Solargraph,RuboCop: both hooks fail on problems present on master for the touched files (verified by restoring each file to master and re-running); the strong-typecheck message set is identical before and after this change (42 problems, same messages).
apiology
added a commit
to apiology/solargraph
that referenced
this pull request
Aug 17, 2026
apiology
added a commit
to apiology/solargraph
that referenced
this pull request
Aug 17, 2026
solargraph typecheck against any project using Forwardable dies before
emitting a single diagnostic:
lib/solargraph/pin/delegated_method.rb:25:in 'initialize':
either :method or :receiver is required (ArgumentError)
from ApiMap#load_with_cache -> catalog -> Store#update ->
combine_duplicate_method_pins -> Pin::Method#combine_with ->
Pin::Base#combine_with.
Pin::Base#combine_with rebuilds the merged pin with
self.class.new(**new_attrs), and new_attrs carries only generic pin
attributes (location, name, closure, comments, visibility, signatures).
Pin::DelegatedMethod#initialize requires exactly one of :method /
:receiver and receives neither, so combining two same-path
DelegatedMethod pins is structurally impossible. This went live when
castwide#1311 started minting DelegatedMethod pins for
def_delegators, which makes duplicate-path groups routine.
combine_duplicate_method_pins already skips groups containing a
Pin::MethodAlias for the same class of reason (a merged pin can't
represent the alias target); DelegatedMethod was never added to that
guard. Extend it rather than teaching DelegatedMethod to merge: a pin
constructed from a :receiver that has since resolved holds both
@receiver_chain and @resolved_method, while initialize forbids passing
both, so any combine_with override would have to discard one pin's
delegation target. When the two pins delegate to different receivers
(reopened class, source-vs-RBS duplicate) that loses information
silently. Keeping both pins preserves it.
Co-Authored-By: Claude Opus 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 18, 2026
A project using Forwardable gets a "Missing @return tag" at strong level for every def_delegators name whose receiver cannot be resolved statically. On plate-spinner that is 42 errors across 15 files, on lines that already carry an @!method/@return directive supplying the type - the directive was written there precisely because the delegation target is unreachable. castwide#1311 mints a Pin::DelegatedMethod per def_delegators name. Where the receiver reaches a method_missing dispatcher, or any object whose declaration cannot be followed, that pin's return type stays undefined. The report comes from TypeChecker#method_tag_problems, which iterates source_map.pins_by_class(Pin::Method) - the raw per-file pin set, not the ApiMap's combined view. It therefore sees the undefined DelegatedMethod pin and never the documented Pin::Method sharing its path. method_return_type_problems_for then special-cases exactly one pin class, `return [] if pin.is_a?(Pin::MethodAlias)`, with no Pin::DelegatedMethod case. Add that case, guarded on resolvable?, which Pin::DelegatedMethod already provides. A delegation that cannot resolve its receiver has no declaration site a @return tag could be written at - the type belongs to the target method, elsewhere - so it is skipped for the same reason MethodAlias is skipped on the line above. Delegations that do resolve are untouched and still enforce their target's type at call sites. Where nothing declares a type, the receiver's own missing tag is still reported, so the root is named once rather than once per delegated name. Negative result, recorded so it is not retried: Store#combine_duplicate_method_pins was tried first, preferring a documented Pin::Method over a same-path DelegatedMethod. It works - the group collapses and get_methods returns the typed pin - and it has no effect on this diagnostic, because method_tag_problems never consults the combined view. That approach was abandoned, not left unfinished. Measured on plate-spinner whole-project at strong: 54 problems in 23 of 501 files before, 12 in 10 after, with all 42 Missing @return cleared. The remaining 12 are @sg-ignore markers other merged fixes made unneeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
A project using Forwardable gets a "Missing @return tag" at strong level for every def_delegators name whose receiver cannot be resolved statically. On plate-spinner that is 42 errors across 15 files, on lines that already carry an @!method/@return directive supplying the type - the directive was written there precisely because the delegation target is unreachable. castwide#1311 mints a Pin::DelegatedMethod per def_delegators name. Where the receiver reaches a method_missing dispatcher, or any object whose declaration cannot be followed, that pin's return type stays undefined. The report comes from TypeChecker#method_tag_problems, which iterates source_map.pins_by_class(Pin::Method) - the raw per-file pin set, not the ApiMap's combined view. It therefore sees the undefined DelegatedMethod pin and never the documented Pin::Method sharing its path. method_return_type_problems_for then special-cases exactly one pin class, `return [] if pin.is_a?(Pin::MethodAlias)`, with no Pin::DelegatedMethod case. Add that case, guarded on resolvable?, which Pin::DelegatedMethod already provides. A delegation that cannot resolve its receiver has no declaration site a @return tag could be written at - the type belongs to the target method, elsewhere - so it is skipped for the same reason MethodAlias is skipped on the line above. Delegations that do resolve are untouched and still enforce their target's type at call sites. Where nothing declares a type, the receiver's own missing tag is still reported, so the root is named once rather than once per delegated name. Negative result, recorded so it is not retried: Store#combine_duplicate_method_pins was tried first, preferring a documented Pin::Method over a same-path DelegatedMethod. It works - the group collapses and get_methods returns the typed pin - and it has no effect on this diagnostic, because method_tag_problems never consults the combined view. That approach was abandoned, not left unfinished. Measured on plate-spinner whole-project at strong: 54 problems in 23 of 501 files before, 12 in 10 after, with all 42 Missing @return cleared. The remaining 12 are @sg-ignore markers other merged fixes made unneeded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
def_delegatorsanddef_delegatordefine real methods, but Solargraph maps neither, so every call to a delegated method is unresolved and any method returning one cannot infer its return type:Separately,
alias_methodin a class body reports a false positive at strong level today:Solution
SendNodemaps both Forwardable forms intoPin::DelegatedMethod— the pin class added in #602, until now constructed only by external plugins — which resolves the receiver and the forwarded method lazily, so a delegated method reports the forwarded method's parameters, return type and signatures, and passes arguments and blocks through. Instance-variable, class-variable and method receivers are supported, as isdef_delegator's optional alias argument. Expansion fires only in a namespace that extendsForwardable;SingleForwardable's class-scope delegation is not covered.The
alias_methodfalse positive shares one cause with what would otherwise have been adef_delegatorsfalse positive: method pins synthesized from a class-body statement cover that statement's range but have no body of their own, so_locate_pintreating them as closures makes the declaring statement resolve in instance scope, wherealias_methodanddef_delegatorsare not available._locate_pinalready skipped attribute pins for this reason, with a@todonoting the hack; aliases and delegations now answer the samePin::Method#body_less?predicate.Test plan
Six strong-level specs — ivar receiver, method receiver, aliased delegation, argument and block passthrough, no expansion without
extend Forwardable, and no false positive on the declaring statement. Five fail without this change. Full suite: 1541 examples, 0 failures, 59 pending.Opened as a draft. This PR was written by Claude (Anthropic's Claude Code) on behalf of @apiology.
🤖 Generated with Claude Code
https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT