From 83fb0eff9074fe5156664f062e3249fba7ff05b0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Aug 2026 11:32:27 -0400 Subject: [PATCH 1/3] Resolve calls to a duck type param's own declared method Chain::Call#resolve converted duck-typed receivers to Object and searched Object's real method stack, so a call matching the duck type's own declared method (e.g. `# @param thing [#read_body]` then `thing.read_body`) was reported as unresolved at strict typecheck levels and above, even though ApiMap#get_complex_type_methods already handles this case correctly for completion. Special-case duck-type receivers to synthesize a matching Pin::DuckMethod, mirroring get_complex_type_methods. The pin is marked non-explicit so arity checking (which has no real signature to check against) is skipped. Fixes https://github.com/castwide/solargraph/issues/1257 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016tARKRqywwAoLhA6DnLCET --- lib/solargraph/source/chain/call.rb | 12 +++++++++--- spec/type_checker/levels/strict_spec.rb | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 52aa1121a..a6ad786f1 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -59,9 +59,15 @@ def resolve api_map, name_pin, locals binder = binder.without_nil if nullable? # @sg-ignore Need to handle duck-typed method calls on union types pin_groups = binder.each_unique_type.map do |context| - ns_tag = context.namespace == '' ? '' : context.namespace_type.tag - stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) - [stack.first].compact + if context.duck_type? && context.name[1..] == word + # explicit: false skips arity checking; the duck type + # only tells us the method exists, not its signature + [Pin::DuckMethod.new(name: word, source: :chain, explicit: false)] + else + ns_tag = context.namespace == '' ? '' : context.namespace_type.tag + stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) + [stack.first].compact + end end pin_groups = [] if !api_map.loose_unions && pin_groups.any?(&:empty?) pins = pin_groups.flatten.uniq(&:path) diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 9f5367138..583eeb242 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -255,6 +255,19 @@ def bar(baz); end expect(checker.problems).to be_empty end + it 'resolves calls to a duck type param\'s own declared method' do + checker = type_checker(%( + class Foo + # @param baz [#read_body] + # @return [void] + def bar(baz) + baz.read_body + end + end + )) + expect(checker.problems).to be_empty + end + it 'reports mismatched duck types' do checker = type_checker(%( class Foo From d0ff6cecf8ed44bd436a8c5869cc4ba24e8f1b14 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Aug 2026 12:37:37 -0400 Subject: [PATCH 2/3] Move duck type own-method regression test to alpha level The fix in Chain::Call#resolve is level-independent, and alpha is the strictest level, so testing there is a stronger guarantee than strict. Claude-Session: https://claude.ai/code/session_016tARKRqywwAoLhA6DnLCET --- spec/type_checker/levels/alpha_spec.rb | 13 +++++++++++++ spec/type_checker/levels/strict_spec.rb | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index aca95b9c3..b2c7ac67e 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -193,6 +193,19 @@ def bing expect(checker.problems.map(&:message)).to eq([]) end + it 'resolves calls to a duck type param\'s own declared method' do + checker = type_checker(%( + class Foo + # @param baz [#read_body] + # @return [void] + def bar(baz) + baz.read_body + end + end + )) + expect(checker.problems).to be_empty + end + it 'resolves self correctly in arguments (second case)' do checker = type_checker(%( class Blah diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 583eeb242..9f5367138 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -255,19 +255,6 @@ def bar(baz); end expect(checker.problems).to be_empty end - it 'resolves calls to a duck type param\'s own declared method' do - checker = type_checker(%( - class Foo - # @param baz [#read_body] - # @return [void] - def bar(baz) - baz.read_body - end - end - )) - expect(checker.problems).to be_empty - end - it 'reports mismatched duck types' do checker = type_checker(%( class Foo From eee8ff14fa742a2a1a822923ff6ce41ea456adca Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Aug 2026 18:56:35 -0400 Subject: [PATCH 3/3] Fix nil closure crash and unqualified super return type in Pin::Method#typify Pin::DuckMethod.new never sets closure:, so the unconditional closure.gates call crashed with NoMethodError on duck-typed method calls. Use the pin's own gates accessor (already nil-safe) instead. typify_from_super also returned an ancestor pin's raw, unqualified return_type instead of calling pin.typify(api_map) on it, so a borrowed type would be qualified against the duck pin's own fabricated gates rather than the ancestor's real ones. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SPC3E2mM9NLYTC7YY3GnG8 --- lib/solargraph/pin/method.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index c1f8f8850..f5d7e2c15 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -294,8 +294,7 @@ def typify api_map type = see_reference(api_map) || typify_from_super(api_map) logger.debug { "Method#typify(self=#{self}) - type=#{type&.rooted_tags.inspect}" } unless type.nil? - # @sg-ignore Need to add nil check here - qualified = type.qualify(api_map, *closure.gates) + qualified = type.qualify(api_map, *gates) logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified end @@ -611,13 +610,14 @@ def method_namespace end # @param api_map [ApiMap] - # @return [ComplexType, nil] + # @return [ComplexType, ComplexType::UniqueType, nil] def typify_from_super api_map stack = rest_of_stack api_map return nil if stack.empty? stack.each do |pin| # @sg-ignore Need to add nil check here - return pin.return_type unless pin.return_type.undefined? + next if pin.return_type.undefined? + return pin.typify(api_map) end nil end