# frozen_string_literal: true
module Solargraph
module Pin
# The base class for method and attribute pins.
#
class Method < Callable
include Solargraph::Parser::NodeMethods
# @return [::Symbol] :public, :private, or :protected
attr_reader :visibility
# @return [Parser::AST::Node]
attr_reader :node
# @param visibility [::Symbol] :public, :protected, or :private
# @param explicit [Boolean]
# @param block [Pin::Signature, nil, ::Symbol]
# @param node [Parser::AST::Node, nil]
# @param attribute [Boolean]
# @param signatures [::Array<Signature>, nil]
# @param anon_splat [Boolean]
def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, **splat
super(**splat)
@visibility = visibility
@explicit = explicit
@block = block
@node = node
@attribute = attribute
@signatures = signatures
@anon_splat = anon_splat
end
def transform_types(&transform)
# @todo 'super' alone should work here I think, but doesn't typecheck at level typed
m = super(&transform)
m.signatures = m.signatures.map do |sig|
sig.transform_types(&transform)
end
m.block = block&.transform_types(&transform)
m.signature_help = nil
m.documentation = nil
m
end
def all_rooted?
super && parameters.all?(&:all_rooted?) && (!block || block&.all_rooted?) && signatures.all?(&:all_rooted?)
end
# @param signature [Pin::Signature]
# @return [Pin::Method]
def with_single_signature(signature)
m = proxy signature.return_type
m.signature_help = nil
m.documentation = nil
# @todo populating the single parameters/return_type/block
# arguments here seems to be needed for some specs to pass,
# even though we have a signature with the same information.
# Is this a problem for RBS-populated methods, which don't
# populate these three?
m.parameters = signature.parameters
m.return_type = signature.return_type
m.block = signature.block
m.signatures = [signature]
m
end
def block?
!block.nil?
end
# @return [Pin::Signature, nil]
def block
return @block unless @block == :undefined
@block = signatures.first&.block
end
def completion_item_kind
attribute? ? Solargraph::LanguageServer::CompletionItemKinds::PROPERTY : Solargraph::LanguageServer::CompletionItemKinds::METHOD
end
def symbol_kind
attribute? ? Solargraph::LanguageServer::SymbolKinds::PROPERTY : LanguageServer::SymbolKinds::METHOD
end
def return_type
@return_type ||= ComplexType.new(signatures.map(&:return_type).flat_map(&:items))
end
# @param parameters [::Array<Parameter>]
# @param return_type [ComplexType]
# @return [Signature]
def generate_signature(parameters, return_type)
block = nil
yieldparam_tags = docstring.tags(:yieldparam)
yieldreturn_tags = docstring.tags(:yieldreturn)
generics = docstring.tags(:generic).map(&:name)
needs_block_param_signature =
parameters.last&.block? || !yieldreturn_tags.empty? || !yieldparam_tags.empty?
if needs_block_param_signature
yield_parameters = yieldparam_tags.map do |p|
name = p.name
decl = :arg
if name
decl = select_decl(name, false)
name = clean_param(name)
end
Pin::Parameter.new(
location: location,
closure: self,
comments: p.text,
name: name,
decl: decl,
presence: location ? location.range : nil,
return_type: ComplexType.try_parse(*p.types)
)
end
yield_return_type = ComplexType.try_parse(*yieldreturn_tags.flat_map(&:types))
block = Signature.new(generics: generics, parameters: yield_parameters, return_type: yield_return_type)
end
Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block)
end
# @return [::Array<Signature>]
def signatures
@signatures ||= begin
top_type = generate_complex_type
result = []
result.push generate_signature(parameters, top_type) if top_type.defined?
result.concat(overloads.map { |meth| generate_signature(meth.parameters, meth.return_type) }) unless overloads.empty?
result.push generate_signature(parameters, @return_type || ComplexType::UNDEFINED) if result.empty?
result
end
end
# @return [String, nil]
def detail
# This property is not cached in an instance variable because it can
# change when pins get proxied.
detail = String.new
detail += if signatures.length > 1
"(*) "
else
"(#{signatures.first.parameters.map(&:full).join(', ')}) " unless signatures.first.parameters.empty?
end.to_s
detail += "=#{probed? ? '~' : (proxied? ? '^' : '>')} #{return_type.to_s}" unless return_type.undefined?
detail.strip!
return nil if detail.empty?
detail
end
# @return [::Array<Hash>]
def signature_help
@signature_help ||= signatures.map do |sig|
{
label: name + '(' + sig.parameters.map(&:full).join(', ') + ')',
documentation: documentation
}
end
end
def desc
# ensure the signatures line up when logged
if signatures.length > 1
"\n#{to_rbs}\n"
else
to_rbs
end
end
def to_rbs
return nil if signatures.empty?
rbs = "def #{name}: #{signatures.first.to_rbs}"
signatures[1..].each do |sig|
rbs += "\n"
rbs += (' ' * (4 + name.length))
rbs += "| #{name}: #{sig.to_rbs}"
end
rbs
end
def path
@path ||= "#{namespace}#{(scope == :instance ? '#' : '.')}#{name}"
end
def typify api_map
decl = super
return decl unless decl.undefined?
type = see_reference(api_map) || typify_from_super(api_map)
return type.qualify(api_map, namespace) unless type.nil?
name.end_with?('?') ? ComplexType::BOOLEAN : ComplexType::UNDEFINED
end
def documentation
if @documentation.nil?
@documentation ||= super || ''
param_tags = docstring.tags(:param)
unless param_tags.nil? or param_tags.empty?
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Params:\n"
lines = []
param_tags.each do |p|
l = "* #{p.name}"
l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? or p.types.empty?
l += " #{p.text}"
lines.push l
end
@documentation += lines.join("\n")
end
yieldparam_tags = docstring.tags(:yieldparam)
unless yieldparam_tags.nil? or yieldparam_tags.empty?
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Block Params:\n"
lines = []
yieldparam_tags.each do |p|
l = "* #{p.name}"
l += " [#{escape_brackets(p.types.join(', '))}]" unless p.types.nil? or p.types.empty?
l += " #{p.text}"
lines.push l
end
@documentation += lines.join("\n")
end
yieldreturn_tags = docstring.tags(:yieldreturn)
unless yieldreturn_tags.empty?
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Block Returns:\n"
lines = []
yieldreturn_tags.each do |r|
l = "*"
l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? or r.types.empty?
l += " #{r.text}"
lines.push l
end
@documentation += lines.join("\n")
end
return_tags = docstring.tags(:return)
unless return_tags.empty?
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Returns:\n"
lines = []
return_tags.each do |r|
l = "*"
l += " [#{escape_brackets(r.types.join(', '))}]" unless r.types.nil? or r.types.empty?
l += " #{r.text}"
lines.push l
end
@documentation += lines.join("\n")
end
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Visibility: #{visibility}"
concat_example_tags
end
@documentation.to_s
end
def explicit?
@explicit
end
def attribute?
@attribute
end
def nearly? other
super &&
parameters == other.parameters &&
scope == other.scope &&
visibility == other.visibility
end
def probe api_map
attribute? ? infer_from_iv(api_map) : infer_from_return_nodes(api_map)
end
def try_merge! pin
return false unless super
@node = pin.node
@resolved_ref_tag = false
true
end
# @return [::Array<Pin::Method>]
def overloads
# Ignore overload tags with nil parameters. If it's not an array, the
# tag's source is likely malformed.
@overloads ||= docstring.tags(:overload).select(&:parameters).map do |tag|
Pin::Signature.new(
generics: generics,
parameters: tag.parameters.map do |src|
name, decl = parse_overload_param(src.first)
Pin::Parameter.new(
location: location,
closure: self,
comments: tag.docstring.all.to_s,
name: name,
decl: decl,
presence: location ? location.range : nil,
return_type: param_type_from_name(tag, src.first)
)
end,
return_type: ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types))
)
end
@overloads
end
def anon_splat?
@anon_splat
end
# @param [ApiMap]
# @return [self]
def resolve_ref_tag api_map
return self if @resolved_ref_tag
@resolved_ref_tag = true
return self unless docstring.ref_tags.any?
docstring.ref_tags.each do |tag|
ref = if tag.owner.to_s.start_with?(/[#\.]/)
api_map.get_methods(namespace)
.select { |pin| pin.path.end_with?(tag.owner.to_s) }
.first
else
# @todo Resolve relative namespaces
api_map.get_path_pins(tag.owner.to_s).first
end
next unless ref
docstring.add_tag(*ref.docstring.tags(:param))
end
self
end
protected
attr_writer :block
attr_writer :signatures
attr_writer :signature_help
attr_writer :documentation
private
# @param name [String]
# @param asgn [Boolean]
#
# @return [::Symbol]
def select_decl name, asgn
if name.start_with?('**')
:kwrestarg
elsif name.start_with?('*')
:restarg
elsif name.start_with?('&')
:blockarg
elsif name.end_with?(':') && asgn
:kwoptarg
elsif name.end_with?(':')
:kwarg
elsif asgn
:optarg
else
:arg
end
end
# @param name [String]
# @return [String]
def clean_param name
name.gsub(/[*&:]/, '')
end
# @param tag [YARD::Tags::OverloadTag]
# @param name [String]
#
# @return [ComplexType]
def param_type_from_name(tag, name)
param = tag.tags(:param).select { |t| t.name == name }.first
return ComplexType::UNDEFINED unless param
ComplexType.try_parse(*param.types)
end
# @return [ComplexType]
def generate_complex_type
tags = docstring.tags(:return).map(&:types).flatten.compact
return ComplexType::UNDEFINED if tags.empty?
ComplexType.try_parse *tags
end
# @param api_map [ApiMap]
# @return [ComplexType, nil]
def see_reference api_map
docstring.ref_tags.each do |ref|
next unless ref.tag_name == 'return' && ref.owner
result = resolve_reference(ref.owner.to_s, api_map)
return result unless result.nil?
end
match = comments.match(/^[ \t]*\(see (.*)\)/m)
return nil if match.nil?
resolve_reference match[1], api_map
end
# @param api_map [ApiMap]
# @return [ComplexType, nil]
def typify_from_super api_map
stack = api_map.get_method_stack(namespace, name, scope: scope).reject { |pin| pin.path == path }
return nil if stack.empty?
stack.each do |pin|
return pin.return_type unless pin.return_type.undefined?
end
nil
end
# @param ref [String]
# @param api_map [ApiMap]
# @return [ComplexType, nil]
def resolve_reference ref, api_map
parts = ref.split(/[\.#]/)
if parts.first.empty? || parts.one?
path = "#{namespace}#{ref}"
else
fqns = api_map.qualify(parts.first, namespace)
return ComplexType::UNDEFINED if fqns.nil?
path = fqns + ref[parts.first.length] + parts.last
end
pins = api_map.get_path_pins(path)
pins.each do |pin|
type = pin.typify(api_map)
return type unless type.undefined?
end
nil
end
# @return [Parser::AST::Node, nil]
def method_body_node
return nil if node.nil?
return node.children[1].children.last if node.type == :DEFN
return node.children[2].children.last if node.type == :DEFS
return node.children[2] if node.type == :def || node.type == :DEFS
return node.children[3] if node.type == :defs
nil
end
# @param api_map [ApiMap]
# @return [ComplexType]
def infer_from_return_nodes api_map
return ComplexType::UNDEFINED if node.nil?
result = []
has_nil = false
return ComplexType::NIL if method_body_node.nil?
returns_from_method_body(method_body_node).each do |n|
if n.nil? || [:NIL, :nil].include?(n.type)
has_nil = true
next
end
rng = Range.from_node(n)
next unless rng
clip = api_map.clip_at(
location.filename,
rng.ending
)
chain = Solargraph::Parser.chain(n, location.filename)
type = chain.infer(api_map, self, clip.locals)
result.push type unless type.undefined?
end
result.push ComplexType::NIL if has_nil
return ComplexType::UNDEFINED if result.empty?
ComplexType.new(result.uniq)
end
# @param [ApiMap] api_map
# @return [ComplexType]
def infer_from_iv api_map
types = []
varname = "@#{name.gsub(/=$/, '')}"
pins = api_map.get_instance_variable_pins(binder.namespace, binder.scope).select { |iv| iv.name == varname }
pins.each do |pin|
type = pin.typify(api_map)
type = pin.probe(api_map) if type.undefined?
types.push type if type.defined?
end
return ComplexType::UNDEFINED if types.empty?
ComplexType.new(types.uniq)
end
# When YARD parses an overload tag, it includes rest modifiers in the parameters names.
#
# @param name [String]
# @return [::Array(String, ::Symbol)]
def parse_overload_param(name)
# @todo this needs to handle mandatory vs not args, kwargs, blocks, etc
if name.start_with?('**')
[name[2..-1], :kwrestarg]
elsif name.start_with?('*')
[name[1..-1], :restarg]
else
[name, :arg]
end
end
# @return [void]
def concat_example_tags
example_tags = docstring.tags(:example)
return if example_tags.empty?
@documentation += "\n\nExamples:\n\n```ruby\n"
@documentation += example_tags.map do |tag|
(tag.name && !tag.name.empty? ? "# #{tag.name}\n" : '') +
"#{tag.text}\n"
end
.join("\n")
.concat("```\n")
end
protected
attr_writer :signatures
end
end
end