Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pin::DelegatedMethod #602

Merged
merged 3 commits into from
Jan 18, 2025
Merged
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
1 change: 1 addition & 0 deletions lib/solargraph/pin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Pin
autoload :Method, 'solargraph/pin/method'
autoload :Signature, 'solargraph/pin/signature'
autoload :MethodAlias, 'solargraph/pin/method_alias'
autoload :DelegatedMethod, 'solargraph/pin/delegated_method'
autoload :BaseVariable, 'solargraph/pin/base_variable'
autoload :InstanceVariable, 'solargraph/pin/instance_variable'
autoload :ClassVariable, 'solargraph/pin/class_variable'
Expand Down
97 changes: 97 additions & 0 deletions lib/solargraph/pin/delegated_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

module Solargraph
module Pin
# A DelegatedMethod is a more complicated version of a MethodAlias that
# allows aliasing a method from a different closure (class/module etc).
class DelegatedMethod < Pin::Method
# A DelegatedMethod can be constructed with either a :resolved_method
# pin, or a :receiver_chain. When a :receiver_chain is supplied, it
# will be used to *dynamically* resolve a receiver type within the
# given closure/scope, and the delegated method will then be resolved
# to a method pin on that type.
#
# @param resolved_method [Method] an already resolved method pin.
# @param receiver [Source::Chain] the source code used to resolve the receiver for this delegated method.
# @param receiver_method_name [String] the method name that will be called on the receiver (defaults to :name).
def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_name: name, **splat)
raise ArgumentError, 'either :method or :receiver is required' if (method && receiver) || (!method && !receiver)
super(name: name, **splat)

@receiver_chain = receiver
@resolved_method = method
@receiver_method_name = receiver_method_name
end

%i[comments parameters return_type location].each do |method|
define_method(method) do
@resolved_method ? @resolved_method.send(method) : super()
end
end

%i[typify realize infer probe].each do |method|
# @param api_map [ApiMap]
define_method(method) do |api_map|
resolve_method(api_map)
@resolved_method ? @resolved_method.send(method, api_map) : super(api_map)
end
end

def resolvable?(api_map)
resolve_method(api_map)
!!@resolved_method
end

private

# Resolves the receiver chain and method name to a method pin, resetting any previously resolution.
#
# @param api_map [ApiMap]
# @return [Pin::Method, nil]
def resolve_method api_map
return if @resolved_method

resolver = @receiver_chain.define(api_map, self, []).first

unless resolver
Solargraph.logger.warn \
"Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'"
return
end

receiver_type = resolver.return_type

return if receiver_type.undefined?

receiver_path, method_scope =
if @receiver_chain.constant?
# HACK: the `return_type` of a constant is Class<Whatever>, but looking up a method expects
# the arguments `"Whatever"` and `scope: :class`.
[receiver_type.to_s.sub(/^Class<(.+)>$/, '\1'), :class]
else
[receiver_type.to_s, :instance]
end

method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope)
@resolved_method = method_stack.first
end

# helper to print a source chain as code, probably not 100% correct.
#
# @param chain [Source::Chain]
def print_chain(chain)
out = +''
chain.links.each_with_index do |link, index|
if index > 0
if Source::Chain::Constant
out << '::' unless link.word.start_with?('::')
else
out << '.'
end
end
out << link.word
end
end
end
end
end
5 changes: 4 additions & 1 deletion lib/solargraph/type_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,10 @@ def argument_problems_for chain, api_map, block_pin, locals, location
base = chain
until base.links.length == 1 && base.undefined?
pins = base.define(api_map, block_pin, locals)
if pins.first.is_a?(Pin::Method)

if pins.first.is_a?(Pin::DelegatedMethod) && !pins.first.resolvable?(api_map)
# Do nothing, as we can't find the actual method implementation
elsif pins.first.is_a?(Pin::Method)
# @type [Pin::Method]
pin = pins.first
ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
Expand Down
40 changes: 40 additions & 0 deletions spec/pin/delegated_method_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'pry'

describe Solargraph::Pin::DelegatedMethod do
it 'can be constructed from a Method pin' do
method_pin = Solargraph::Pin::Method.new(comments: '@return [Hash<String, String>]')

delegation_pin = Solargraph::Pin::DelegatedMethod.new(method: method_pin, scope: :instance)
expect(delegation_pin.return_type.to_s).to eq('Hash<String, String>')
end

it 'can be constructed from a receiver source and method name' do
api_map = Solargraph::ApiMap.new
source = Solargraph::Source.load_string(%(
class Class1
# @return [String]
def name; end
end

class Class2
# @return [Class1]
def collaborator; end
end
))
api_map.map source

class2 = api_map.get_path_pins('Class2').first

chain = Solargraph::Source::Chain.new([Solargraph::Source::Chain::Call.new('collaborator')])
pin = Solargraph::Pin::DelegatedMethod.new(
closure: class2,
scope: :instance,
name: 'name',
receiver: chain
)

pin.probe(api_map)

expect(pin.return_type.to_s).to eq('String')
end
end