Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions lib/solargraph/pin/duck_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,31 @@ module Pin
# use duck typing, e.g., `@param file [#read]`.
#
class DuckMethod < Pin::Method
# @param location [Solargraph::Location]
# @param name [String]
# def initialize location, name
# # super(location, '', name, nil, :instance, :public, [])
# end
# A duck-type tag has no syntax for arguments, so the default empty
# #parameters would synthesize a zero-arg signature no real call matches.
#
# @param splat [Hash{Symbol => Object}]
def initialize **splat
super(parameters: accepts_any_arguments, **splat)
end

# @return [::Array<Pin::Parameter>]
def accepts_any_arguments
[
Pin::Parameter.new(decl: :restarg, name: 'args', closure: self, source: :api_map),
Pin::Parameter.new(decl: :kwrestarg, name: 'kwargs', closure: self, source: :api_map)
]
end

# A synthetic pin sits in no ancestor chain and has no closure. Letting
# the inherited walk find a type sends #typify on to `closure.gates`,
# which raises.
#
# @param _api_map [ApiMap]
# @return [Array<Pin::Method>]
def rest_of_stack _api_map
[]
end
end
end
end
12 changes: 9 additions & 3 deletions lib/solargraph/source/chain/call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions spec/pin/duck_method_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

describe Solargraph::Pin::DuckMethod do
let(:api_map) do
Solargraph::ApiMap.new.tap do |map|
map.map Solargraph::Source.load_string(%(
class ClassTest
# @param clazz [#new]
def create_object(clazz)
clazz.new
end
end
))
end
end

# The one place these are built: a duck-type tag reaches ApiMap via its
# parsed ComplexType, not via the source that declared it.
let(:duck_new) do
api_map.get_complex_type_methods(Solargraph::ComplexType.parse('#new'))
.grep(described_class).first
end

# ApiMap leaves #closure nil; the call site is what gives an ancestor
# walk somewhere to go, since ClassTest inherits its own Class#new.
let(:duck_new_at_call_site) do
described_class.new(name: 'new', source: :api_map,
closure: api_map.get_path_pins('ClassTest#create_object').first)
end

it 'synthesizes a signature that accepts any arguments' do
expect(duck_new.signatures.first.parameters.map(&:decl)).to eq(%i[restarg kwrestarg])
end

it 'has no ancestor chain of its own to walk' do
expect(duck_new_at_call_site.rest_of_stack(api_map)).to be_empty
end

it "infers nothing rather than the call site's own inherited #new" do
expect(duck_new_at_call_site.typify(api_map)).to be_undefined
end
end
13 changes: 13 additions & 0 deletions spec/type_checker/levels/alpha_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading