diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f58633a0c..a1fc7eb6a 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -236,6 +236,21 @@ def keyword_pins store.pins_by_class(Pin::Keyword) end + # An array of pins based on factory parameters. + # + # @return [Enumerable] + 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] + 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] @@ -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] def get_path_pins path diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index a5870ff50..59b8a4a62 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -68,6 +68,11 @@ def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end + # @return [Hash{String => Array}] + def factory_parameter_hash + @factory_parameter_hash ||= Hash.new { |h, k| h[k] = [] } + end + # @param pins [Enumerable] # @return [self] def merge pins @@ -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 @@ -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 @@ -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 diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index f3e2ed278..1b57bd7aa 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -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] + def factory_parameters_for_method(method_pin) + factory_parameter_hash[method_pin.path] || [] + end + private # @return [Index] @@ -307,6 +315,11 @@ def extend_references index.extend_references end + # @return [Hash{String => Array}] + def factory_parameter_hash + index.factory_parameter_hash + end + # @param name [String] # @return [Enumerable] def namespace_children name diff --git a/lib/solargraph/language_server/message/text_document/definition.rb b/lib/solargraph/language_server/message/text_document/definition.rb index 5f143cc82..2c1ecc506 100644 --- a/lib/solargraph/language_server/message/text_document/definition.rb +++ b/lib/solargraph/language_server/message/text_document/definition.rb @@ -24,7 +24,10 @@ def code_location # @return [Array] 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) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 9d5162431..c49325f35 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -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] diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index d8d46319b..f562e4c84 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -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, nil] + def initialize node, filename = nil, parent = nil, tree = nil @node = node @filename = filename @parent = parent + @tree = tree end # @return [Source::Chain] @@ -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] @@ -130,6 +133,13 @@ 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) + end else lit = infer_literal_node_type(n) result.push (lit ? Chain::Literal.new(lit, n) : Chain::Link.new) @@ -137,6 +147,15 @@ def generate_links n 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 diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 526ac6fc3..0e26f2244 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -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 diff --git a/lib/solargraph/pin/factory_parameter.rb b/lib/solargraph/pin/factory_parameter.rb new file mode 100644 index 000000000..a3a758af7 --- /dev/null +++ b/lib/solargraph/pin/factory_parameter.rb @@ -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 + # @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 diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index 33f02e027..f92978a35 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -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 diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index c08d04878..f7fde900b 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -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 @@ -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 diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index cd89a5d85..2f2f8a466 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -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? @@ -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] diff --git a/lib/solargraph/source/chain/literal.rb b/lib/solargraph/source/chain/literal.rb index 2e0d65c9e..1e81f3b76 100644 --- a/lib/solargraph/source/chain/literal.rb +++ b/lib/solargraph/source/chain/literal.rb @@ -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 diff --git a/lib/solargraph/source/chain/parameter.rb b/lib/solargraph/source/chain/parameter.rb new file mode 100644 index 000000000..4fb57185b --- /dev/null +++ b/lib/solargraph/source/chain/parameter.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Solargraph + class Source + class Chain + # A method parameter represented as a link in a method call chain. + # + # @example + # some_method('literal_value') + # ^^^^^^^^^^^^^ + class Parameter < Link + # @param literal [Chain::Literal] - literal argument + # @param method_call_chain [Chain] method call that contains the argument + def initialize literal, method_call_chain # rubocop:disable Lint/MissingSuper + @literal = literal + @method_call_chain = method_call_chain + end + + def word + @word ||= "#{method_name}(.., #{@literal.word}, ..)" + end + + # @param api_map [ApiMap] + # @param name_pin [Pin::Base] + # @param locals [::Array] + # @return [::Array] + def resolve api_map, name_pin, locals + # @type [Pin::Method] + method_pin = method_call_chain.define(api_map, name_pin, locals)&.first + return [] unless method_pin + + # Use indexed lookup for better performance + api_map.factory_parameters_for_method(method_pin).select do |fp| + param_index = method_pin.parameters.find_index { |param| param.name == fp.param_name } + next if param_index.nil? + + fp.value == literal_value && current_index == param_index + end + end + + # @return [Boolean] true if this is a parameter of Kernel#require + def require_parameter? + method_call_chain.links.last.word == 'require' + end + + private + + # @return [Chain::Literal] + attr_reader :literal + # @ return [Chain] + attr_reader :method_call_chain + + # @return [String] The name of the method that this parameter belongs to + def method_name + @method_name ||= method_call.word + end + + # @return [Chain::Call] + def method_call + @method_call_chain.links.last + end + + # The index of the literal in the method call chain. + # @return [Integer] + def current_index + @current_index ||= method_call_chain.node.children[2..].index(literal.node) + end + + # @return [::String, ::Symbol] The literal value of the parameter + # @sg-ignore false "return type could not be inferred" + def literal_value + literal.node.children.first + end + end + end + end +end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index 5758a9d35..21b543424 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -31,34 +31,18 @@ def initialize source, position # @return [Source::Chain] def chain - # Special handling for files that end with an integer and a period - return Chain.new([Chain::Literal.new('Integer', Integer(phrase[0..-2])), Chain::UNDEFINED_CALL]) if phrase =~ /^[0-9]+\.$/ - return Chain.new([Chain::Literal.new('Symbol', phrase[1..].to_sym)]) if phrase.start_with?(':') && !phrase.start_with?('::') return SourceChainer.chain(source, Position.new(position.line, position.character + 1)) if end_of_phrase.strip == '::' && source.code[Position.to_offset(source.code, position)].to_s.match?(/[a-z]/i) + begin - return Chain.new([]) if phrase.end_with?('..') - node = nil - parent = nil - if !source.repaired? && source.parsed? && source.synchronized? - tree = source.tree_at(position.line, position.column) - node, parent = tree[0..2] - elsif source.parsed? && source.repaired? && end_of_phrase == '.' - node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] - node = Parser.parse(fixed_phrase) if node.nil? - elsif source.repaired? - node = Parser.parse(fixed_phrase) - else - node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] unless source.error_ranges.any?{|r| r.nil? || r.include?(fixed_position)} - # Exception for positions that chain literal nodes in unsynchronized sources - node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil? - node = Parser.parse(fixed_phrase) if node.nil? - end + node, parent, tree = node_from_position rescue Parser::SyntaxError return Chain.new([Chain::UNDEFINED_CALL]) end + return Chain.new([Chain::UNDEFINED_CALL]) if node.nil? || (node.type == :sym && !phrase.start_with?(':')) - # chain = NodeChainer.chain(node, source.filename, parent && parent.type == :block) - chain = Parser.chain(node, source.filename, parent) + + chain = Parser.chain(node, source.filename, parent, tree) + if source.repaired? || !source.parsed? || !source.synchronized? if end_of_phrase.strip == '.' chain.links.push Chain::UNDEFINED_CALL @@ -73,6 +57,38 @@ def chain private + # @return [Array(Parser::AST::Node, Parser::AST::Node, Array)] + # parent node, child node, and the tree at the position + def node_from_position + node = nil + parent = nil + tree = nil + + if source.parsed? && !source.repaired? && source.synchronized? + tree = source.tree_at(position.line, position.column) + node, parent = tree[0..2] + elsif source.parsed? && source.repaired? && end_of_phrase == '.' + tree = source.tree_at(fixed_position.line, fixed_position.column) + node, parent = tree[0..2] + node = Parser.parse(fixed_phrase) if node.nil? + tree = source.tree_at(fixed_position.line, fixed_position.column) if node + elsif source.repaired? + node = Parser.parse(fixed_phrase) + tree = source.tree_at(fixed_position.line, fixed_position.column) if node + else + unless source.error_ranges.any? { |r| r.nil? || r.include?(fixed_position) } + tree = source.tree_at(fixed_position.line, fixed_position.column) + node, parent = tree[0..2] + end + # Exception for positions that chain literal nodes in unsynchronized sources + node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil? + node = Parser.parse(fixed_phrase) if node.nil? + tree = source.tree_at(fixed_position.line, fixed_position.column) if node + end + + [node, parent, tree] + end + # @return [Position] attr_reader :position @@ -81,12 +97,12 @@ def chain # @return [String] def phrase - @phrase ||= source.code[signature_data..offset-1] + @phrase ||= source.code[signature_data..offset - 1] end # @return [String] def fixed_phrase - @fixed_phrase ||= phrase[0..-(end_of_phrase.length+1)] + @fixed_phrase ||= phrase[0..-(end_of_phrase.length + 1)] end # @return [Position] @@ -137,7 +153,7 @@ def get_signature_data_at index brackets = 0 squares = 0 parens = 0 - index -=1 + index -= 1 in_whitespace = false while index >= 0 pos = Position.from_offset(@source.code, index) @@ -148,18 +164,16 @@ def get_signature_data_at index if brackets.zero? and parens.zero? and squares.zero? and [' ', "\r", "\n", "\t"].include?(char) in_whitespace = true else - if brackets.zero? and parens.zero? and squares.zero? and in_whitespace - unless char == '.' or @source.code[index+1..-1].strip.start_with?('.') - old = @source.code[index+1..-1] - nxt = @source.code[index+1..-1].lstrip - index += (@source.code[index+1..-1].length - @source.code[index+1..-1].lstrip.length) - break - end + if brackets.zero? and parens.zero? and squares.zero? and in_whitespace && !(char == '.' or @source.code[index + 1..-1].strip.start_with?('.')) + @source.code[index + 1..-1] + @source.code[index + 1..-1].lstrip + index += (@source.code[index + 1..-1].length - @source.code[index + 1..-1].lstrip.length) + break end if char == ')' - parens -=1 + parens -= 1 elsif char == ']' - squares -=1 + squares -= 1 elsif char == '}' brackets -= 1 elsif char == '(' @@ -175,9 +189,7 @@ def get_signature_data_at index break if char == '$' if char == '@' index -= 1 - if @source.code[index, 1] == '@' - index -= 1 - end + index -= 1 if @source.code[index, 1] == '@' break end elsif parens == 1 || brackets == 1 || squares == 1 diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index a934d0850..4929e5e7a 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -158,6 +158,11 @@ def map source end end + # @return [Array] + def convention_pins + @convention_pins || [] + end + private # @return [Hash{Class => Array}] @@ -170,11 +175,6 @@ def data @data ||= Data.new(source) end - # @return [Array] - def convention_pins - @convention_pins || [] - end - # @param pins [Array] # @return [Array] def convention_pins=(pins) diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 16a4ec845..609b08b5d 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -12,13 +12,13 @@ def initialize api_map, cursor @api_map = api_map @cursor = cursor block_pin = block - block_pin.rebind(api_map) if block_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(block_pin.receiver).contain?(cursor.range.start) + block_pin.rebind(api_map) if block_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(block_pin.receiver)&.contain?(cursor.range.start) @in_block = nil end # @return [Array] Relevant pins for infering the type of the Cursor's position def define - return [] if cursor.comment? || cursor.chain.literal? + return [] if cursor.comment? || cursor.chain.literal? || cursor.chain.require_parameter? result = cursor.chain.define(api_map, block, locals) result.concat file_global_methods result.concat((source_map.pins + source_map.locals).select{ |p| p.name == cursor.word && p.location.range.contain?(cursor.position) }) if result.empty? @@ -33,7 +33,8 @@ def types # @return [Completion] def complete return package_completions([]) if !source_map.source.parsed? || cursor.string? - return package_completions(api_map.get_symbols) if cursor.chain.literal? && cursor.chain.links.last.word == '' + # TODO: Improve magic word comparsion == '<::Symbol>', too fragile + return package_completions(api_map.get_symbols) if cursor.chain.literal? && cursor.chain.links.last.word == '<::Symbol>' return Completion.new([], cursor.range) if cursor.chain.literal? if cursor.comment? tag_complete diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index abc8c2b05..057d40246 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -48,6 +48,13 @@ expect(chain.literal?).to be(true) end + it "recognizes require parameters" do + method_call_source = Solargraph::Source.load_string('require "some_file"') + chain = Solargraph::Source::SourceChainer.chain(method_call_source, Solargraph::Position.new(0, 13)) + expect(chain.links.last).to be_a(Solargraph::Source::Chain::Parameter) + expect(chain.require_parameter?).to be(true) + end + it "recognizes constants" do chain = described_class.new([Solargraph::Source::Chain::Constant.new('String')]) expect(chain.constant?).to be(true) @@ -80,6 +87,88 @@ def meth; end expect(type.name).to eq('Sub') end + it 'infers types from factory methods' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def local _source_map + Solargraph::Environ.new( + pins: [ + Solargraph::Pin::FactoryParameter.new( + method_name: 'create', + method_namespace: 'FactoryBot', + method_scope: :class, + param_name: 'klass', + value: :my_model, + return_type: Solargraph::ComplexType.parse('MyModel') + ) + ] + ) + end + end + + Solargraph::Convention.register dummy_convention + + source = Solargraph::Source.new(%( + class MyModel + end + module FactoryBot + def self.create(klass, **args) + end + end + + FactoryBot.create(:my_model, name: 'test') + ), 'test.rb') + source_map = Solargraph::SourceMap.map(source) + + api_map = Solargraph::ApiMap.new + api_map.index source_map.pins + source_map.convention_pins + chain = Solargraph::Source::SourceChainer.chain(source_map.source, Solargraph::Position.new(8, 20)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) + expect(type.name).to eq('MyModel') + Solargraph::Convention.deregister dummy_convention + end + + it "defines factory parameters" do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def local _source_map + Solargraph::Environ.new( + pins: [ + Solargraph::Pin::FactoryParameter.new( + method_name: 'create', + method_namespace: 'FactoryBot', + method_scope: :class, + param_name: 'klass', + value: :my_model, + return_type: Solargraph::ComplexType.parse('MyModel'), + location: Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 6, 0, 18)) # eg. class MyModel + ) + ] + ) + end + end + + Solargraph::Convention.register dummy_convention + + source = Solargraph::Source.new(%( + class MyModel + end + module FactoryBot + def self.create(klass, **args) + end + end + + FactoryBot.create(:my_model, name: 'test') + ), 'test.rb') + source_map = Solargraph::SourceMap.map(source) + + api_map = Solargraph::ApiMap.new + api_map.index source_map.pins + source_map.convention_pins + chain = Solargraph::Source::SourceChainer.chain(source_map.source, Solargraph::Position.new(8, 30)) + pins = chain.define(api_map, Solargraph::Pin::ROOT_PIN, []) + expect(pins.length).to eq(1) + expect(pins.first).to be_a(Solargraph::Pin::FactoryParameter) + Solargraph::Convention.deregister dummy_convention + end + it "follows constant chains" do source = Solargraph::Source.load_string(%( module Mixin; end diff --git a/spec/source/source_chainer_spec.rb b/spec/source/source_chainer_spec.rb index 7a8eb9fb8..adc6b5ed3 100644 --- a/spec/source/source_chainer_spec.rb +++ b/spec/source/source_chainer_spec.rb @@ -80,6 +80,21 @@ expect(cursor.chain).to be_undefined end + it "recognizes method literal parameters" do + source = Solargraph::Source.load_string(<<~RUBY) + SomeClass + .instance + .foo(:bar, "baz", 1) + RUBY + map = Solargraph::SourceMap.map(source) + cursor = map.cursor_at(Solargraph::Position.new(2, 10)) # :bar + expect(cursor.chain.links.last).to be_a(Solargraph::Source::Chain::Parameter) + expect(cursor.chain.links.map(&:word)).to eq(['foo(.., <::Symbol>, ..)']) + cursor = map.cursor_at(Solargraph::Position.new(2, 16)) # "baz" + expect(cursor.chain.links.last).to be_a(Solargraph::Source::Chain::Parameter) + expect(cursor.chain.links.map(&:word)).to eq(['foo(.., <::String>, ..)']) + end + it "chains signatures with square brackets" do map = Solargraph::SourceMap.load_string('foo[0].bar') cursor = map.cursor_at(Solargraph::Position.new(0, 8)) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..1cc3182da 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,3 +43,8 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = 'tmp/rspec_status.txt' +end