-
-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
2 changed files
with
97 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# 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). | ||
# | ||
# This is implemented in two parts: the | ||
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_chain [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 && method.name), receiver_method_name: name, **splat) | ||
raise ArgumentError, 'either :method or :chain is required' if (method && receiver) || (!method && !receiver) | ||
|
||
super(name: name, **splat) | ||
|
||
# @type [Source::Chain] | ||
@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 | ||
|
||
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 |