Skip to content
Open
18 changes: 18 additions & 0 deletions lib/solargraph/api_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,21 @@ def keyword_pins
store.pins_by_class(Pin::Keyword)
end

# An array of pins based on factory parameters.
#
# @return [Enumerable<Solargraph::Pin::FactoryParameter>]
def factory_parameter_pins
store.pins_by_class(Pin::FactoryParameter)
end

# Get factory parameters for a method pin.
#
# @param method_pin [Solargraph::Pin::Method]
# @return [Array<Solargraph::Pin::FactoryParameter>]
def factory_parameters_for_method(method_pin)
store.factory_parameters_for_method(method_pin)
end

# An array of namespace names defined in the ApiMap.
#
# @return [Set<String>]
Expand Down Expand Up @@ -567,6 +582,9 @@ def get_path_suggestions path

# Get an array of pins that match the specified path.
#
# @example
# api_map.get_pins_by_path('String#split')
#
# @param path [String]
# @return [Enumerable<Pin::Base>]
def get_path_pins path
Expand Down
15 changes: 14 additions & 1 deletion lib/solargraph/api_map/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ def superclass_references
@superclass_references ||= Hash.new { |h, k| h[k] = [] }
end

# @return [Hash{String => Array<Pin::FactoryParameter>}]
def factory_parameter_hash
@factory_parameter_hash ||= Hash.new { |h, k| h[k] = [] }
end

# @param pins [Enumerable<Pin::Base>]
# @return [self]
def merge pins
Expand All @@ -77,7 +82,7 @@ def merge pins
protected

attr_writer :pins, :pin_select_cache, :namespace_hash, :pin_class_hash, :path_pin_hash, :include_references,
:extend_references, :prepend_references, :superclass_references
:extend_references, :prepend_references, :superclass_references, :factory_parameter_hash

# @return [self]
def deep_clone
Expand Down Expand Up @@ -113,6 +118,7 @@ def catalog new_pins
map_references Pin::Reference::Extend, extend_references
map_references Pin::Reference::Superclass, superclass_references
map_include_pins
map_factory_parameters
map_overrides
self
end
Expand Down Expand Up @@ -191,6 +197,13 @@ def redefine_return_type pin, tag
end
pin.reset_generated!
end

# @return [void]
def map_factory_parameters
pins_by_class(Pin::FactoryParameter).each do |fp|
factory_parameter_hash[fp.method_path] << fp
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/solargraph/api_map/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ def get_ancestors(fqns)
ancestors.compact.uniq
end

# Get factory parameters for a method pin.
#
# @param method_pin [Pin::Method]
# @return [Array<Pin::FactoryParameter>]
def factory_parameters_for_method(method_pin)
factory_parameter_hash[method_pin.path] || []
end

private

# @return [Index]
Expand Down Expand Up @@ -307,6 +315,11 @@ def extend_references
index.extend_references
end

# @return [Hash{String => Array<Pin::FactoryParameter>}]
def factory_parameter_hash
index.factory_parameter_hash
end

# @param name [String]
# @return [Enumerable<Solargraph::Pin::Base>]
def namespace_children name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def code_location

# @return [Array<Hash>]
def require_location
# @todo Terrible hack
# @todo Terrible hack - move this logic to [Solargraph::Source::Chain::Parameter]
# @example
# require 'click_me'
# ^^^^^^^^^^
lib = host.library_for(params['textDocument']['uri'])
rloc = Solargraph::Location.new(uri_to_file(params['textDocument']['uri']), Solargraph::Range.from_to(@line, @column, @line, @column))
dloc = lib.locate_ref(rloc)
Expand Down
8 changes: 2 additions & 6 deletions lib/solargraph/library.rb
Original file line number Diff line number Diff line change
Expand Up @@ -563,12 +563,8 @@ def handle_file_not_found filename, error
def maybe_map source
return unless source
return unless @current == source || workspace.has_file?(source.filename)
if source_map_hash.key?(source.filename)
new_map = Solargraph::SourceMap.map(source)
source_map_hash[source.filename] = new_map
else
source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
end

source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
end

# @return [Set<Gem::Specification>]
Expand Down
25 changes: 22 additions & 3 deletions lib/solargraph/parser/parser_gem/node_chainer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ class NodeChainer
# @param node [Parser::AST::Node]
# @param filename [String, nil]
# @param parent [Parser::AST::Node, nil]
def initialize node, filename = nil, parent = nil
# @param tree [Array<Parser::AST::Node>, nil]
def initialize node, filename = nil, parent = nil, tree = nil
@node = node
@filename = filename
@parent = parent
@tree = tree
end

# @return [Source::Chain]
Expand All @@ -29,9 +31,10 @@ class << self
# @param node [Parser::AST::Node]
# @param filename [String, nil]
# @param parent [Parser::AST::Node, nil]
# @param tree [Parser::AST::Node, nil]
# @return [Source::Chain]
def chain node, filename = nil, parent = nil
NodeChainer.new(node, filename, parent).chain
def chain node, filename = nil, parent = nil, tree = nil
NodeChainer.new(node, filename, parent, tree).chain
end

# @param code [String]
Expand Down Expand Up @@ -130,13 +133,29 @@ def generate_links n
elsif n.type == :array
chained_children = n.children.map { |c| NodeChainer.chain(c) }
result.push Source::Chain::Array.new(chained_children, n)
elsif inside_method_call? # If the node is inside a method call, it may be a parameter.
lit = infer_literal_node_type(n)
if lit
method_call_chain = NodeChainer.chain(send_node, @filename, nil, [])
literal = Chain::Literal.new(lit, n)
result.push Chain::Parameter.new(literal, method_call_chain)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I wanted to make this work as a chain of: method_call > parameter; however, I struggled to find a good way to pass the last method call resolution to the Chain::Parameter as a context here:

return [] if undefined?
# working_pin is the surrounding closure pin for the link
# being processed, whose #binder method will provide the LHS /
# 'self type' of the next link (same as the #return_type method
# --the type of the result so far).
#
# @todo ProxyType uses 'type' for the binder, but '
working_pin = name_pin
links[0..-2].each do |link|
pins = link.resolve(api_map, working_pin, locals)
type = infer_from_definitions(pins, working_pin, api_map, locals)
if type.undefined?
logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) => [] - undefined type from #{link.desc}" }
return []
end
# We continue to use the context from the head pin, in case
# we need it to, for instance, provide context for a block
# evaluation. However, we use the last link's return type
# for the binder, as this is chaining off of it, and the
# binder is now the lhs of the rhs we are evaluating.
working_pin = Pin::ProxyType.anonymous(name_pin.context, binder: type, closure: name_pin, source: :chain)
logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) - after processing #{link.desc}, new working_pin=#{working_pin} with binder #{working_pin.binder}" }
end
links.last.last_context = working_pin
links.last.resolve(api_map, working_pin, locals)

So I ended up with parameter(method_call_chain).

Maybe I don't understand the Pin::ProxyType well enough, so if you have a suggestion on how to make it work, I'd be more than happy to refactor this into an "appropriate chain".

end
else
lit = infer_literal_node_type(n)
result.push (lit ? Chain::Literal.new(lit, n) : Chain::Link.new)
end
result
end

def inside_method_call?
!send_node.nil? && send_node != @node
end

# @return [Parser::AST::Node, nil]
def send_node
@send_node ||= @tree&.find { |n| n.type == :send }
end

# @param node [Parser::AST::Node]
def hash_is_splatted? node
return false unless Parser.is_ast_node?(node) && node.type == :hash
Expand Down
1 change: 1 addition & 0 deletions lib/solargraph/pin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module Pin
autoload :Until, 'solargraph/pin/until'
autoload :While, 'solargraph/pin/while'
autoload :Callable, 'solargraph/pin/callable'
autoload :FactoryParameter, 'solargraph/pin/factory_parameter'

ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil, source: :pin_rb)
end
Expand Down
84 changes: 84 additions & 0 deletions lib/solargraph/pin/factory_parameter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Solargraph
module Pin
# A method parameter with a literal value used in a factory method.
#
# @example
# RSpec.shared_examples 'some examples'
#
# FactoryParameter.new(
# method_name: 'shared_examples',
# method_namespace: 'RSpec',
# method_scope: :class,
# param_name: 'name',
# value: 'some examples',
# decl: :arg,
# return_type: 'void'
# )
class FactoryParameter < Base
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to other naming suggestions!

# @return [String]
attr_reader :method_name
# @return [String]
attr_reader :method_namespace
# @return [Symbol] :class or :instance
attr_reader :method_scope
# @return [String, nil]
attr_reader :param_name
# @return [::String, ::Symbol] The literal value
attr_reader :value
# @return decl [::Symbol] :arg, :optarg, :kwarg, :kwoptarg, :restarg, :kwrestarg, :block, :blockarg
attr_reader :decl
# @return [Location, nil]
attr_reader :location

# @param method_name [String] The name of the method that this parameter belongs to
# @param method_namespace [String] The class of the method that this parameter belongs to
# @param method_scope [Symbol] The scope of the method, either :class or :instance
# @param param_name [String, nil] The name of the parameter
# @param value [String, Symbol] The value of the parameter
# @param decl [::Symbol] :arg, :kwarg
# @param location [Location, nil] The location of the parameter in the source code
# @param return_type [String, nil] The return type of the method that this parameter belongs to
def initialize method_name:,
method_namespace:,
method_scope:,
param_name:,
value:,
return_type:,
decl: :arg,
location: nil
super(location: location)
@method_name = method_name
@method_namespace = method_namespace
@method_scope = method_scope
@param_name = param_name
@value = value
@decl = decl
@return_type = return_type
end

def name
param_name
end

def text_documentation
"#{method_path}(#{param_name}) = #{value.inspect}"
end

# @return [String]
def method_path
# @sg-ignore false failure on method_scope "Wrong argument type for Solargraph::Pin::Base#==: other expected
# Solargraph::Pin::Base, received :instance"
@method_path ||= "#{method_namespace}#{method_scope == :instance ? '#' : '.'}#{method_name}"
end

private

def inner_desc
"method_path=#{method_path}, value=#{value.inspect}, decl=#{decl.inspect}, " \
"return_type=#{return_type&.to_s || 'nil'}"
end
end
end
end
2 changes: 1 addition & 1 deletion lib/solargraph/pin/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def do_query
# @param str2 [String]
# @return [Float]
def fuzzy_string_match str1, str2
return (1.0 + (str2.length.to_f / str1.length.to_f)) if str1.downcase.include?(str2.downcase)
return 1.0 + (str2.length.to_f / str1.length.to_f) if str1.downcase.include?(str2.downcase)
JaroWinkler.similarity(str1, str2, ignore_case: true)
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/solargraph/source/chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Chain
autoload :ZSuper, 'solargraph/source/chain/z_super'
autoload :Hash, 'solargraph/source/chain/hash'
autoload :Array, 'solargraph/source/chain/array'
autoload :Parameter, 'solargraph/source/chain/parameter'

@@inference_stack = []
@@inference_depth = 0
Expand Down Expand Up @@ -172,6 +173,13 @@ def literal?
links.last.is_a?(Chain::Literal)
end

# @return [Boolean]
# @sg-ignore false "return type could not be inferred"
def require_parameter?
# @sg-ignore false "Unresolved call to require_parameter? on Solargraph::Source::Chain::Link"
links.last.is_a?(Chain::Parameter) && links.last.require_parameter?
end

def undefined?
links.any?(&:undefined?)
end
Expand Down
27 changes: 26 additions & 1 deletion lib/solargraph/source/chain/call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ def inferred_pins pins, api_map, name_pin, locals
#
# qualify(), however, happens in the namespace where
# the docs were written - from the method pin.
type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, p.namespace) if new_return_type.defined?
if new_return_type.defined?
type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, p.namespace)
else
type = inferr_from_factory_parameters(api_map, p)
end
type ||= ComplexType::UNDEFINED
end
break if type.defined?
Expand Down Expand Up @@ -166,6 +170,27 @@ def inferred_pins pins, api_map, name_pin, locals
end
end

# @param api_map [ApiMap]
# @param method_pin [Pin::Method]
# @return [ComplexType, nil]
def inferr_from_factory_parameters(api_map, method_pin)
factory_parameter = api_map.factory_parameters_for_method(method_pin).find do |factory_param|
method_pin.parameters.each_with_index.find do |param, index|
current_argument = arguments[index]
next unless current_argument&.literal?
# @type [Solargraph::Source::Chain::Literal]
last_link = current_argument.links.last
argument_value = last_link.value

param.name == factory_param.param_name && argument_value == factory_param.value
end
end

return nil if factory_parameter.nil?

factory_parameter.return_type.qualify(api_map, method_pin.namespace)
end

# @param pin [Pin::Base]
# @param api_map [ApiMap]
# @param context [ComplexType]
Expand Down
6 changes: 6 additions & 0 deletions lib/solargraph/source/chain/literal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ def word
@word ||= "<#{@type}>"
end

# @return [::String, ::Symbol]
attr_reader :value

# @return [Parser::AST::Node]
attr_reader :node

# @param type [String]
# @param node [Parser::AST::Node, Object]
def initialize type, node
@node = node

if node.is_a?(::Parser::AST::Node)
if node.type == :true
@value = true
Expand Down
Loading
Loading