From 8c538d65f4d418ff061689d4436cc8a4e939f379 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Sat, 10 Sep 2022 10:46:18 +0200 Subject: [PATCH 1/3] 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. --- lib/solargraph/pin.rb | 1 + lib/solargraph/pin/delegated_method.rb | 92 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 lib/solargraph/pin/delegated_method.rb diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 8e251338f..7795a8710 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -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' diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb new file mode 100644 index 000000000..c60240ddb --- /dev/null +++ b/lib/solargraph/pin/delegated_method.rb @@ -0,0 +1,92 @@ +# 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 + + 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, 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 From 69a0a71de809e3b1a89fc0980ba3df9a6a2a5de2 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Sun, 8 Jan 2023 14:29:21 +0100 Subject: [PATCH 2/3] Start a spec for Pin::DelegatedMethod --- spec/pin/delegated_method_spec.rb | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 spec/pin/delegated_method_spec.rb diff --git a/spec/pin/delegated_method_spec.rb b/spec/pin/delegated_method_spec.rb new file mode 100644 index 000000000..551f63928 --- /dev/null +++ b/spec/pin/delegated_method_spec.rb @@ -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]') + + delegation_pin = Solargraph::Pin::DelegatedMethod.new(method: method_pin, scope: :instance) + expect(delegation_pin.return_type.to_s).to eq('Hash') + 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 From 145618b8dc01b97913d9e3dfbd243ccc1047d80b Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Mon, 9 Jan 2023 00:05:52 +0100 Subject: [PATCH 3/3] Don't report arity problems for unresolvable delegated methods --- lib/solargraph/pin/delegated_method.rb | 5 +++++ lib/solargraph/type_checker.rb | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index c60240ddb..25aab0986 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -37,6 +37,11 @@ def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_n 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. diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 059caaeff..35c90b0cf 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -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)