Skip to content

Commit

Permalink
Add Pin::DelegatedMethod (#602)
Browse files Browse the repository at this point in the history
* Add Pin::DelegatedMethod

This is a more complicated version of a method alias, where the type
of the method receiver can be resolved dynamically using a Source::Chain.

As hinted at by the name, this allows creating pins that fully support
the `Module.delegate` method from ActiveSupports core extensions.

* Start a spec for Pin::DelegatedMethod

* Don't report arity problems for unresolvable delegated methods
  • Loading branch information
grncdr authored Jan 18, 2025
1 parent 07c1c2a commit 987e402
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 1 deletion.
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 @@ -264,7 +264,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

0 comments on commit 987e402

Please sign in to comment.