# frozen_string_literal: true
module Solargraph
class Source
class Chain
class Call < Chain::Link
include Solargraph::Parser::NodeMethods
# @return [String]
attr_reader :word
# @return [::Array<Chain>]
attr_reader :arguments
# @return [Chain, nil]
attr_reader :block
# @param word [String]
# @param arguments [::Array<Chain>]
# @param block [Chain, nil]
def initialize word, arguments = [], block = nil
@word = word
@arguments = arguments
@block = block
fix_block_pass
end
def with_block?
!!@block
end
# @param api_map [ApiMap]
# @param name_pin [Pin::Closure] name_pin.binder should give us the object on which 'word' will be invoked
# @param locals [::Array<Pin::LocalVariable>]
def resolve api_map, name_pin, locals
return super_pins(api_map, name_pin) if word == 'super'
return yield_pins(api_map, name_pin) if word == 'yield'
found = if head?
locals.select { |p| p.name == word }
else
[]
end
return inferred_pins(found, api_map, name_pin, locals) unless found.empty?
pins = name_pin.binder.each_unique_type.flat_map do |context|
ns = context.namespace == '' ? '' : context.namespace_type.tag
api_map.get_method_stack(ns, word, scope: context.scope)
end
return [] if pins.empty?
inferred_pins(pins, api_map, name_pin, locals)
end
private
# @param pins [::Enumerable<Pin::Method>]
# @param api_map [ApiMap]
# @param name_pin [Pin::Base]
# @param locals [::Array<Pin::LocalVariable>]
# @return [::Array<Pin::Base>]
def inferred_pins pins, api_map, name_pin, locals
result = pins.map do |p|
next p unless p.is_a?(Pin::Method)
overloads = p.signatures
# next p if overloads.empty?
type = ComplexType::UNDEFINED
# start with overloads that require blocks; if we are
# passing a block, we want to find a signature that will
# use it. If we didn't pass a block, the logic below will
# reject it regardless
sorted_overloads = overloads.sort { |ol| ol.block? ? -1 : 1 }
new_signature_pin = nil
sorted_overloads.each do |ol|
next unless ol.arity_matches?(arguments, with_block?)
match = true
atypes = []
arguments.each_with_index do |arg, idx|
param = ol.parameters[idx]
if param.nil?
match = ol.parameters.any?(&:restarg?)
break
end
atype = atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(name_pin.context), locals)
# make sure we get types from up the method
# inheritance chain if we don't have them on this pin
ptype = param.typify api_map
# @todo Weak type comparison
# unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag)
unless ptype.undefined? || atype.name == ptype.name || ptype.any? { |current_ptype| api_map.super_and_sub?(current_ptype.name, atype.name) } || ptype.generic? || param.restarg?
match = false
break
end
end
if match
if ol.block && with_block?
block_atypes = ol.block.parameters.map(&:return_type)
if block.links.map(&:class) == [BlockSymbol]
# like the bar in foo(&:bar)
blocktype = block_symbol_call_type(api_map, name_pin.context, block_atypes, locals)
else
blocktype = block_call_type(api_map, name_pin, locals)
end
end
new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes, nil, nil, blocktype)
new_return_type = new_signature_pin.return_type
type = with_params(new_return_type.self_to_type(name_pin.context), name_pin.context).qualify(api_map, name_pin.context.namespace) if new_return_type.defined?
type ||= ComplexType::UNDEFINED
end
break if type.defined?
end
p = p.with_single_signature(new_signature_pin) unless new_signature_pin.nil?
next p.proxy(type) if type.defined?
if !p.macros.empty?
result = process_macro(p, api_map, name_pin.context, locals)
next result unless result.return_type.undefined?
elsif !p.directives.empty?
result = process_directive(p, api_map, name_pin.context, locals)
next result unless result.return_type.undefined?
end
p
end
result.map do |pin|
if pin.path == 'Class#new' && name_pin.context.tag != 'Class'
reduced_context = name_pin.context.reduce_class_type
pin.proxy(reduced_context)
else
next pin if pin.return_type.undefined?
selfy = pin.return_type.self_to_type(name_pin.context)
selfy == pin.return_type ? pin : pin.proxy(selfy)
end
end
end
# @param pin [Pin::Base]
# @param api_map [ApiMap]
# @param context [ComplexType]
# @param locals [Enumerable<Pin::Base>]
# @return [Pin::Base]
def process_macro pin, api_map, context, locals
pin.macros.each do |macro|
# @todo 'Wrong argument type for
# Solargraph::Source::Chain::Call#inner_process_macro:
# macro expected YARD::Tags::MacroDirective, received
# generic<Elem>' is because we lose 'rooted' information
# in the 'Chain::Array' class internally, leaving
# ::Array#each shadowed when it shouldn't be.
result = inner_process_macro(pin, macro, api_map, context, locals)
return result unless result.return_type.undefined?
end
Pin::ProxyType.anonymous(ComplexType::UNDEFINED)
end
# @param pin [Pin::Method]
# @param api_map [ApiMap]
# @param context [ComplexType]
# @param locals [Enumerable<Pin::Base>]
# @return [Pin::ProxyType]
def process_directive pin, api_map, context, locals
pin.directives.each do |dir|
macro = api_map.named_macro(dir.tag.name)
next if macro.nil?
result = inner_process_macro(pin, macro, api_map, context, locals)
return result unless result.return_type.undefined?
end
Pin::ProxyType.anonymous ComplexType::UNDEFINED
end
# @param pin [Pin::Base]
# @param macro [YARD::Tags::MacroDirective]
# @param api_map [ApiMap]
# @param context [ComplexType]
# @param locals [Enumerable<Pin::Base>]
# @return [Pin::ProxyType]
def inner_process_macro pin, macro, api_map, context, locals
vals = arguments.map{ |c| Pin::ProxyType.anonymous(c.infer(api_map, pin, locals)) }
txt = macro.tag.text.clone
if txt.empty? && macro.tag.name
named = api_map.named_macro(macro.tag.name)
txt = named.tag.text.clone if named
end
i = 1
vals.each do |v|
txt.gsub!(/\$#{i}/, v.context.namespace)
i += 1
end
docstring = Solargraph::Source.parse_docstring(txt).to_docstring
tag = docstring.tag(:return)
unless tag.nil? || tag.types.nil?
return Pin::ProxyType.anonymous(ComplexType.try_parse(*tag.types))
end
Pin::ProxyType.anonymous(ComplexType::UNDEFINED)
end
# @param docstring [YARD::Docstring]
# @param context [ComplexType]
# @return [ComplexType, nil]
def extra_return_type docstring, context
if docstring.has_tag?('return_single_parameter') #&& context.subtypes.one?
return context.subtypes.first || ComplexType::UNDEFINED
elsif docstring.has_tag?('return_value_parameter') && context.value_types.one?
return context.value_types.first
end
nil
end
# @param name_pin [Pin::Base]
# @return [Pin::Method, nil]
def find_method_pin(name_pin)
method_pin = name_pin
until method_pin.is_a?(Pin::Method)
method_pin = method_pin.closure
return if method_pin.nil?
end
method_pin
end
# @param api_map [ApiMap]
# @param name_pin [Pin::Base]
# @return [::Array<Pin::Base>]
def super_pins api_map, name_pin
method_pin = find_method_pin(name_pin)
return [] if method_pin.nil?
pins = api_map.get_method_stack(method_pin.namespace, method_pin.name, scope: method_pin.context.scope)
pins.reject{|p| p.path == name_pin.path}
end
# @param api_map [ApiMap]
# @param name_pin [Pin::Base]
# @return [::Array<Pin::Base>]
def yield_pins api_map, name_pin
method_pin = find_method_pin(name_pin)
return [] unless method_pin
method_pin.signatures.map(&:block).compact.map do |signature_pin|
return_type = signature_pin.return_type.qualify(api_map, name_pin.namespace)
signature_pin.proxy(return_type)
end
end
# @param type [ComplexType]
# @param context [ComplexType]
# @return [ComplexType]
def with_params type, context
return type unless type.to_s.include?('$')
ComplexType.try_parse(type.to_s.gsub('$', context.value_types.map(&:rooted_tag).join(', ')).gsub('<>', ''))
end
# @return [void]
def fix_block_pass
argument = @arguments.last&.links&.first
@block = @arguments.pop if argument.is_a?(BlockSymbol) || argument.is_a?(BlockVariable)
end
# @param api_map [ApiMap]
# @param context [ComplexType]
# @param block_parameter_types [::Array<ComplexType>]
# @param locals [::Array<Pin::LocalVariable>]
# @return [ComplexType, nil]
def block_symbol_call_type(api_map, context, block_parameter_types, locals)
# Ruby's shorthand for sending the passed in method name
# to the first yield parameter with no arguments
block_symbol_name = block.links.first.word
block_symbol_call_path = "#{block_parameter_types.first}##{block_symbol_name}"
callee = api_map.get_path_pins(block_symbol_call_path).first
return_type = callee&.return_type
# @todo: Figure out why we get unresolved generics at
# this point and need to assume method return types
# based on the generic type
return_type ||= api_map.get_path_pins("#{context.subtypes.first}##{block.links.first.word}").first&.return_type
return_type || ComplexType::UNDEFINED
end
# @param api_map [ApiMap]
# @return [Pin::Block, nil]
def find_block_pin(api_map)
node_location = Solargraph::Location.from_node(block.node)
return if node_location.nil?
block_pins = api_map.get_block_pins
block_pins.find { |pin| pin.location.contain?(node_location) }
end
# @param api_map [ApiMap]
# @param name_pin [Pin::Base]
# @param block_parameter_types [::Array<ComplexType>]
# @param locals [::Array<Pin::LocalVariable>]
# @return [ComplexType, nil]
def block_call_type(api_map, name_pin, locals)
return nil unless with_block?
block_context_pin = name_pin
block_pin = find_block_pin(api_map)
block_context_pin = block_pin.closure if block_pin
block.infer(api_map, block_context_pin, locals)
end
end
end
end
end