diff --git a/.yardopts b/.yardopts index b5adca9f9..d5e994511 100644 --- a/.yardopts +++ b/.yardopts @@ -1,2 +1,3 @@ lib/**/*.rb --plugin yard-solargraph +--plugin activesupport-concern diff --git a/README.md b/README.md index 0acaa61fe..3ade339f5 100755 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ See [https://solargraph.org/guides](https://solargraph.org/guides) for more tips ### Development +To see more logging when typechecking or running specs, set the +`SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is +the default value. + Code contributions are always appreciated. Feel free to fork the repo and submit pull requests. Check for open issues that could use help. Start new issues to discuss changes that have a major impact on the code or require large time commitments. ### Sponsorship and Donation diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 6a0edc5a4..55b47051f 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -168,8 +168,12 @@ def cursor_at filename, position # @param position [Position, Array(Integer, Integer)] # @return [SourceMap::Clip] def clip_at filename, position + logger.debug { "ApiMap#clip_at(filename=#{filename}, position=#{position}) - start" } + position = Position.normalize(position) - clip(cursor_at(filename, position)) + out = clip(cursor_at(filename, position)) + logger.debug { "ApiMap#clip_at(filename=#{filename}, position=#{position}) => #{out}" } + out end # Create an ApiMap with a workspace in the specified directory. @@ -254,6 +258,7 @@ def namespace_exists? name, context = '' # @param contexts [Array] The contexts # @return [Array] def get_constants namespace, *contexts + logger.debug { "ApiMap#get_constants(namespace=#{namespace.inspect}, contexts=#{contexts.inspect})" } namespace ||= '' contexts.push '' if contexts.empty? cached = cache.get_constants(namespace, contexts) @@ -262,11 +267,13 @@ def get_constants namespace, *contexts result = [] contexts.each do |context| fqns = qualify(namespace, context) + logger.debug { "ApiMap#get_constants(namespace=#{namespace.inspect}, contexts=#{contexts.inspect}) - fqns=#{fqns}" } visibility = [:public] visibility.push :private if fqns == context result.concat inner_get_constants(fqns, visibility, skip) end cache.set_constants(namespace, contexts, result) + logger.debug { "ApiMap#get_constants(namespace=#{namespace.inspect}, contexts=#{contexts.inspect}) => #{result}" } result end @@ -323,6 +330,7 @@ def qualify tag, context_tag = '' # @return [String, nil] fully qualified namespace def qualify_namespace(namespace, context_namespace = '') cached = cache.get_qualified_namespace(namespace, context_namespace) + logger.debug { "ApiMap#qualify_namespace(namespace=#{namespace.inspect}, context_namespace=#{context_namespace.inspect}) - cached=#{cached.inspect}" } return cached.clone unless cached.nil? result = if namespace.start_with?('::') inner_qualify(namespace[2..-1], '', Set.new) @@ -330,6 +338,7 @@ def qualify_namespace(namespace, context_namespace = '') inner_qualify(namespace, context_namespace, Set.new) end cache.set_qualified_namespace(namespace, context_namespace, result) + logger.debug { "ApiMap#qualify_namespace(namespace=#{namespace.inspect}, context_namespace=#{context_namespace.inspect}) => #{result.inspect}" } result end @@ -362,7 +371,9 @@ def visible_pins(*args, **kwargs, &blk) # @param namespace [String] A fully qualified namespace # @return [Enumerable] def get_class_variable_pins(namespace) - prefer_non_nil_variables(store.get_class_variables(namespace)) + out = prefer_non_nil_variables(store.get_class_variables(namespace)) + logger.debug { "ApiMap#get_class_variable_pins(namespace=#{namespace.inspect}) => #{out}" } + out end # @return [Enumerable] @@ -707,6 +718,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false # namespaces; resolving the generics in the method pins is this # class' responsibility methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort{ |a, b| a.name <=> b.name } + methods = methods.map(&:as_virtual_class_method) if store.get_includes(fqns).include?('ActiveSupport::Concern') && scope == :class + logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" } result.concat methods if deep if scope == :instance @@ -719,6 +732,31 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core) end else + store.get_includes(fqns).reverse.each do |include_tag| + rooted_include_tag = qualify(include_tag, rooted_tag) + logger.debug { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + module_extends = store.get_extends(rooted_include_tag) + # ActiveSupport::Concern is syntactic sugar for a common + # pattern to include class methods while mixing-in a Module + + # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html + logger.debug { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + if module_extends.include? 'ActiveSupport::Concern' + unless rooted_include_tag.nil? + # yard-activesupport-concern pulls methods inside + # 'class_methods' blocks into main class visible from YARD + included_class_pins = inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, :class, visibility, deep, skip, true) + result.concat included_class_pins + + # another pattern is to put class methods inside a submodule + classmethods_include_tag = rooted_include_tag + "::ClassMethods" + included_classmethods_pins = inner_get_methods_from_reference(classmethods_include_tag, namespace_pin, rooted_type, :instance, visibility, deep, skip, true) + result.concat included_classmethods_pins + end + end + end + + logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" } store.get_extends(fqns).reverse.each do |em| fqem = qualify(em, fqns) result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil? @@ -781,14 +819,20 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop # @param skip [Set] # @return [Array] def inner_get_constants fqns, visibility, skip - return [] if fqns.nil? || skip.include?(fqns) + logger.debug { "ApiMap#inner_get_constants(fqns=#{fqns.inspect}, visibility=#{visibility.inspect}, skip=#{skip.inspect}) - starting" } + if fqns.nil? || skip.include?(fqns) + logger.debug { "ApiMap#inner_get_constants(fqns=#{fqns.inspect}, visibility=#{visibility.inspect}, skip=#{skip.inspect}) => [] - fqns=#{fqns.inspect}, skip=#{skip}" } + return [] + end skip.add fqns result = [] store.get_prepends(fqns).each do |is| result.concat inner_get_constants(qualify(is, fqns), [:public], skip) end - result.concat store.get_constants(fqns, visibility) - .sort { |a, b| a.name <=> b.name } + constants = store.get_constants(fqns, visibility) + .sort { |a, b| a.name <=> b.name } + logger.debug { "Constants in #{fqns} with visibility #{visibility}, constants=#{constants}" } + result.concat constants store.get_includes(fqns).each do |is| result.concat inner_get_constants(qualify(is, fqns), [:public], skip) end @@ -796,6 +840,7 @@ def inner_get_constants fqns, visibility, skip unless %w[Object BasicObject].include?(fqsc) result.concat inner_get_constants(fqsc, [:public], skip) end + logger.debug { "ApiMap#inner_get_constants(fqns=#{fqns.inspect}, visibility=#{visibility.inspect}, skip=#{skip.inspect}) => #{result}" } result end @@ -831,6 +876,7 @@ def qualify_superclass fq_sub_tag # @param skip [Set] Contexts already searched # @return [String, nil] Fully qualified ("rooted") namespace def inner_qualify name, root, skip + logger.debug { "ApiMap#inner_qualify(name=#{name.inspect}, root=#{root.inspect}, skip=#{skip.inspect}) - starting" } return name if name == ComplexType::GENERIC_TAG_NAME return nil if name.nil? return nil if skip.include?(root) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 47f92194c..eac090918 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -129,7 +129,9 @@ def get_symbols # @param fqns [String] # @return [Boolean] def namespace_exists?(fqns) - fqns_pins(fqns).any? + out = fqns_pins(fqns).any? + logger.debug { "Store#namespace_exists?(#{fqns.inspect}) => #{out}" } + out end # @return [Set] @@ -195,9 +197,13 @@ def fqns_pins fqns base = '' name = fqns end - fqns_pins_map[[base, name]] + out = fqns_pins_map[[base, name]] + logger.debug { "Store#fqns_pins(#{fqns.inspect}) => #{out}" } + out end + include Logging + private def index diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 9e23eb502..cf2d92154 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -34,13 +34,16 @@ def initialize types = [UniqueType::UNDEFINED] # @param context [String] # @return [ComplexType] def qualify api_map, context = '' + logger.debug { "ComplexType#qualify(self=#{self.rooted_tags}, context=#{context.inspect}) - starting" } red = reduce_object types = red.items.map do |t| next t if ['nil', 'void', 'undefined'].include?(t.name) next t if ['::Boolean'].include?(t.rooted_name) t.qualify api_map, context end - ComplexType.new(types).reduce_object + out = ComplexType.new(types).reduce_object + logger.debug { "ComplexType#qualify(self=#{self.rooted_tags}, context=#{context.inspect}) => #{out.rooted_tags}" } + out end # @param generics_to_resolve [Enumerable]] @@ -225,7 +228,9 @@ def force_rooted # @return [ComplexType] def resolve_generics definitions, context_type result = @items.map { |i| i.resolve_generics(definitions, context_type) } - ComplexType.new(result) + out = ComplexType.new(result) + # logger.debug { "ComplexType#resolve_generics(self=#{rooted_tags}, definitions=#{definitions}, context_type=#{context_type.rooted_tags} => #{out.rooted_tags}" } + out end def nullable? @@ -405,6 +410,8 @@ def try_parse *strings BOOLEAN = ComplexType.parse('::Boolean') BOT = ComplexType.parse('bot') + include Logging + private # @todo This is a quick and dirty hack that forces `self` keywords diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 0f4ec430d..6e5e3fb26 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -311,7 +311,7 @@ def resolve_param_generics_from_context(generics_to_resolve, context_type, resol def resolve_generics definitions, context_type return self if definitions.nil? || definitions.generics.empty? - transform(name) do |t| + out = transform(name) do |t| if t.name == GENERIC_TAG_NAME generic_name = t.subtypes.first&.name idx = definitions.generics.index(generic_name) @@ -337,6 +337,8 @@ def resolve_generics definitions, context_type t end end + # logger.debug { "UniqueType#resolve_generics(self=#{self.rooted_tag}, definitions=#{definitions}, context_type=#{context_type.rooted_tags}) => #{out}" } + out end # @yieldparam t [self] @@ -410,7 +412,7 @@ def transform(new_name = nil, &transform_type) # @param context [String] The namespace from which to resolve names # @return [self, ComplexType, UniqueType] The generated ComplexType def qualify api_map, context = '' - transform do |t| + out = transform do |t| next t if t.name == GENERIC_TAG_NAME next t if t.duck_type? || t.void? || t.undefined? recon = (t.rooted? ? '' : context) @@ -421,6 +423,8 @@ def qualify api_map, context = '' end t.recreate(new_name: fqns, make_rooted: true) end + logger.debug { "UniqueType#qualify(self=#{rooted_tags.inspect}, context=#{context}) => #{out.rooted_tags.inspect}" } + out end def selfy? @@ -468,7 +472,6 @@ def self.can_root_name?(name) '::NilClass' => UniqueType::NIL }.freeze - include Logging end end diff --git a/lib/solargraph/convention/gemspec.rb b/lib/solargraph/convention/gemspec.rb index 01175a0b1..59e4d6b4d 100644 --- a/lib/solargraph/convention/gemspec.rb +++ b/lib/solargraph/convention/gemspec.rb @@ -12,7 +12,8 @@ def local source_map 'Gem::Specification.new', %( @yieldparam [self] - ) + ), + source: :gemspec, ) ] ) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index f36ea1442..b3abb5925 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -90,9 +90,16 @@ def cache_rbs_collection_pins(gemspec, out) # @param gemspec [Gem::Specification] def cache(gemspec, rebuild: false, out: nil) - out.puts("Caching pins for gem #{gemspec.name}:#{gemspec.version}") if out - cache_yard_pins(gemspec, out) if uncached_yard_gemspecs.include?(gemspec) || rebuild - cache_rbs_collection_pins(gemspec, out) if uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild + build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild + build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild + if build_yard || build_rbs_collection + type = [] + type << 'YARD' if build_yard + type << 'RBS collection' if build_rbs_collection + out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out + end + cache_yard_pins(gemspec, out) if build_yard + cache_rbs_collection_pins(gemspec, out) if build_rbs_collection end # @return [Array] @@ -121,10 +128,14 @@ def rbs_collection_pins_in_memory self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {} end - def combined_pins_in_memory + def self.all_combined_pins_in_memory @combined_pins_in_memory ||= {} end + def combined_pins_in_memory + self.class.all_combined_pins_in_memory + end + # @return [Set] def dependencies @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index b92cbd6af..733639b8c 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -46,7 +46,7 @@ def self.combine_method_pins(*pins) end # @param yard_pins [Array] - # @param rbs_map [RbsMap] + # @param rbs_pins [Array] # @return [Array] def self.combine(yard_pins, rbs_pins) in_yard = Set.new diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index c2875011d..f96e8d6d2 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -436,17 +436,6 @@ def bench ) end - # Get an array of foldable ranges for the specified file. - # - # @deprecated The library should not need to handle folding ranges. The - # source itself has all the information it needs. - # - # @param filename [String] - # @return [Array] - def folding_ranges filename - read(filename).folding_ranges - end - # Create a library from a directory. # # @param directory [String] The path to be used for the workspace @@ -587,7 +576,7 @@ def cache_errors # @return [void] def cache_next_gemspec - return if @cache_progres + return if @cache_progress spec = (api_map.uncached_yard_gemspecs + api_map.uncached_rbs_collection_gemspecs). find { |spec| !cache_errors.include?(spec) } return end_cache_progress unless spec diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index a26610fee..e6773101d 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -11,18 +11,42 @@ module Logging 'info' => Logger::INFO, 'debug' => Logger::DEBUG } - - @@logger = Logger.new(STDERR, level: DEFAULT_LOG_LEVEL) + configured_level = ENV['SOLARGRAPH_LOG'] + level = if LOG_LEVELS.keys.include?(configured_level) + LOG_LEVELS.fetch(configured_level) + else + STDERR.puts("Invalid value for SOLARGRAPH_LOG: #{configured_level.inspect} - valid values are #{LOG_LEVELS.keys}") if configured_level + DEFAULT_LOG_LEVEL + end + @@logger = Logger.new(STDERR, level: level) # @sg-ignore Fix cvar issue @@logger.formatter = proc do |severity, datetime, progname, msg| "[#{severity}] #{msg}\n" end + @@dev_null_logger = Logger.new('/dev/null') + module_function + # override this in your class to temporarily set a custom + # filtering log level for the class (e.g., suppress any debug + # message by setting it to :info even if it is set elsewhere, or + # show existing debug messages by setting to :debug). @return + # [Symbol] + def log_level + :warn + end + # @return [Logger] def logger - @@logger + if LOG_LEVELS[log_level.to_s] == DEFAULT_LOG_LEVEL + @@logger + else + new_log_level = LOG_LEVELS[log_level.to_s] + logger = Logger.new(STDERR, level: new_log_level) + logger.formatter = @@logger.formatter + logger + end end end end diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index d493ccb67..45a4391c6 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -35,6 +35,8 @@ def process process_children end + include Logging + private # @param subregion [Region] diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index 32bb186dd..355deeb13 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -21,7 +21,10 @@ def initialize node, filename = nil, parent = nil # @return [Source::Chain] def chain links = generate_links(@node) - Chain.new(links, @node, (Parser.is_ast_node?(@node) && @node.type == :splat)) + logger.debug { "NodeChainer#chain(@node=#{@node}, @filename=#{@filename}, @parent=#{@parent}) - links=#{links}" } + out = Chain.new(links, @node, (Parser.is_ast_node?(@node) && @node.type == :splat)) + logger.debug { "NodeChainer#chain(@node=#{@node}, @filename=#{@filename}, @parent=#{@parent}) => #{out}" } + out end class << self @@ -43,6 +46,8 @@ def load_string(code) end end + include Logging + private # @param n [Parser::AST::Node] diff --git a/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb index d6426a448..07a8fa62e 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb @@ -7,7 +7,7 @@ module NodeProcessors class CvasgnNode < Parser::NodeProcessor::Base def process loc = get_node_location(node) - pins.push Solargraph::Pin::ClassVariable.new( + pin = Solargraph::Pin::ClassVariable.new( location: loc, closure: region.closure, name: node.children[0].to_s, @@ -15,8 +15,12 @@ def process assignment: node.children[1], source: :parser ) + logger.debug { "CvasgnNode#process() - pin=#{pin}" } + pins.push pin process_children end + + include Logging end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb index 7d4fd2136..cd03ce0e4 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -23,16 +23,18 @@ def process # @param superclass_name [String, nil] def process_namespace(superclass_name) loc = get_node_location(node) + name = unpack_name(node.children[0]) nspin = Solargraph::Pin::Namespace.new( type: node.type, location: loc, closure: region.closure, - name: unpack_name(node.children[0]), + name: name, comments: comments_for(node), visibility: :public, gates: region.closure.gates.freeze, source: :parser ) + logger.debug { "NamespaceNode#process: Created namespace pin: #{nspin} in closure #{region.closure} and namespace=#{nspin.namespace} and name=#{name}" } pins.push nspin unless superclass_name.nil? pins.push Pin::Reference::Superclass.new( diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 49e066b2c..c1d30d6ef 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -50,6 +50,7 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @identity = nil @docstring = docstring @directives = directives + assert_location_provided assert_source_provided end @@ -304,6 +305,12 @@ def choose_pin_attr(other, attr) [val1, val2].compact.min_by { _1.best_location.to_s } end + def assert_location_provided + return unless best_location.nil? && [:yardoc, :source, :rbs].include?(source) + + Solargraph.assert_or_log(:best_location, "Neither location nor type_location provided - #{path} #{source} #{self.class}") + end + def assert_source_provided Solargraph.assert_or_log(:source, "source not provided - #{@path} #{@source} #{self.class}") if source.nil? end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 15cdd918f..359668fb8 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -75,6 +75,7 @@ def return_types_from_node(parent_node, api_map) types.push result unless result.undefined? end end + logger.debug { "BaseVariable#return_types_from_node(#{parent_node}) => #{types.map(&:rooted_tags)}" } types end @@ -112,6 +113,8 @@ def type_desc "#{super} = #{assignment&.type.inspect}" end + include Logging + private # @return [ComplexType] diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 5cb6a691d..e06e94fae 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -49,33 +49,54 @@ def destructure_yield_types(yield_types, parameters) # @param api_map [ApiMap] # @return [::Array] def typify_parameters(api_map) + logger.debug("Block#typify_parameters() - start") chain = Parser.chain(receiver, filename, node) + logger.debug { "Block#typify_parameters() - chain=#{chain.desc}" } clip = api_map.clip_at(location.filename, location.range.start) locals = clip.locals - [self] meths = chain.define(api_map, closure, locals) + logger.debug { "Block#typify_parameters() - meths=#{meths}" } # @todo Convert logic to use signatures meths.each do |meth| - next if meth.block.nil? - + if meth.block.nil? + logger.debug { "Block#typify_parameters() - no block for #{meth.path} - moving to next method: #{meth}" } + logger.debug { "Block#typify_parameters() - meth.signatures: #{meth.signatures}" } + next + end + logger.debug { "Block#typify_parameters() - meth.block=#{meth.block}" } yield_types = meth.block.parameters.map(&:return_type) + logger.debug { "Block#typify_parameters() - yield_types were #{yield_types.map(&:rooted_tags)} from #{meth.path}: #{meth}" } # 'arguments' is what the method says it will yield to the # block; 'parameters' is what the block accepts argument_types = destructure_yield_types(yield_types, parameters) param_types = argument_types.each_with_index.map do |arg_type, idx| + logger.debug { "Block#typify_parameters() - looking at argument #{idx} - type is #{arg_type}" } param = parameters[idx] + logger.debug { "Block#typify_parameters() - looking at param=#{param}" } param_type = chain.base.infer(api_map, param, locals) + logger.debug { "Block#typify_parameters() - param_type=#{param_type}" } unless arg_type.nil? if arg_type.generic? && param_type.defined? namespace_pin = api_map.get_namespace_pins(meth.namespace, closure.namespace).first - arg_type.resolve_generics(namespace_pin, param_type) + after_generics = arg_type.resolve_generics(namespace_pin, param_type) + logger.debug { "Block#typify_parameters() - arg_type=#{arg_type}, namespace_pin=#{namespace_pin}, param_type=#{param_type}, after_generics=#{after_generics}" } + after_generics else arg_type.self_to_type(chain.base.infer(api_map, self, locals)).qualify(api_map, meth.context.namespace) end end end - return param_types if param_types.all?(&:defined?) + if param_types.all?(&:defined?) + logger.debug { "Block#typify_parameters() => #{param_types.map(&:rooted_tags)}" } + return param_types + else + logger.debug { "Block#typify_parameters() - param_types=#{param_types.map(&:rooted_tags)}" } + end end - parameters.map { ComplexType::UNDEFINED } + logger.debug { "Block#typify_parameters(): methods provided no information" } + out = parameters.map { ComplexType::UNDEFINED } + logger.debug { "Block#typify_parameters() => #{out.map(&:rooted_tags)}" } + out end private @@ -86,17 +107,27 @@ def maybe_rebind api_map return ComplexType::UNDEFINED unless receiver chain = Parser.chain(receiver, location.filename) + logger.debug { "Block#maybe_rebind(): chain: #{chain}" } locals = api_map.source_map(location.filename).locals_at(location) receiver_pin = chain.define(api_map, closure, locals).first return ComplexType::UNDEFINED unless receiver_pin + logger.debug { "Block#maybe_rebind(): receiver_pin: #{receiver_pin}" } types = receiver_pin.docstring.tag(:yieldreceiver)&.types - return ComplexType::UNDEFINED unless types&.any? + unless types&.any? + logger.debug { "Block#maybe_rebind(): no yield receiver types => undefined" } + return ComplexType::UNDEFINED + end + logger.debug { "Block#maybe_rebind(): yield receiver tag types: #{types}" } target = chain.base.infer(api_map, receiver_pin, locals) target = full_context unless target.defined? - ComplexType.try_parse(*types).qualify(api_map, receiver_pin.context.namespace).self_to_type(target) + logger.debug { "Block#maybe_rebind(): target=#{target}" } + + out = ComplexType.try_parse(*types).qualify(api_map, receiver_pin.context.namespace).self_to_type(target) + logger.debug { "Block#maybe_rebind() => #{out}" } + out end end end diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 551ba5522..1047292d4 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -69,6 +69,8 @@ def rbs_generics '[' + generics.map { |gen| gen.to_s }.join(', ') + '] ' end + + include Logging end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 7fc343630..60ef6fb0c 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -22,7 +22,8 @@ class Method < Callable # @param attribute [Boolean] # @param signatures [::Array, nil] # @param anon_splat [Boolean] - def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, **splat + def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, + **splat super(**splat) @visibility = visibility @explicit = explicit @@ -199,9 +200,11 @@ def generate_signature(parameters, return_type) ) 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, source: source, closure: self) + block = Signature.new(generics: generics, parameters: yield_parameters, return_type: yield_return_type, source: source, + closure: self, location: location, type_location: type_location) end - signature = Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, closure: self, source: source) + signature = Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, closure: self, source: source, + location: location, type_location: type_location) block.closure = signature if block signature end @@ -296,7 +299,13 @@ def typify api_map logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified end - super + if name.end_with?('?') + logger.debug { "Method#typify(self=#{self}) => Boolean (? suffix)" } + ComplexType::BOOLEAN + else + logger.debug { "Method#typify(self=#{self}) => undefined" } + ComplexType::UNDEFINED + end end # @sg-ignore diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index bc802b748..e51fa2ccb 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -156,8 +156,30 @@ def index # @param api_map [ApiMap] def typify api_map - return return_type.qualify(api_map, closure.context.namespace) unless return_type.undefined? - closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) + logger.debug { "Parameter#typify(self=#{self.desc} in #{closure.desc}) - starting" } + unless return_type.undefined? + out = return_type.qualify(api_map, closure.context.namespace) + logger.debug { "Parameter#typify(self=#{self.desc}, return_type=#{return_type.rooted_tags}, ) => #{out.rooted_tags} from declaration" } + return out + end + if closure.is_a?(Pin::Block) + out = typify_block_param(api_map) + logger.debug { "Parameter#typify(self=#{self.desc}) => #{out.rooted_tags} from block parameter" } + out + else + out = typify_method_param(api_map) + logger.debug { "Parameter#typify(self=#{self.desc}) => #{out.rooted_tags} from method parameter" } + out + end + end + + # @param atype [ComplexType] + # @param api_map [ApiMap] + def compatible_arg?(atype, api_map) + # make sure we get types from up the method + # inheritance chain if we don't have them on this pin + ptype = typify api_map + ptype.undefined? || ptype.can_assign?(api_map, atype) || ptype.generic? end # @param atype [ComplexType] @@ -175,6 +197,8 @@ def documentation tag.text end + include Logging + private # @return [YARD::Tags::Tag, nil] @@ -189,6 +213,7 @@ def param_tag # @param api_map [ApiMap] # @return [ComplexType] def typify_block_param api_map + logger.debug { "Parameter#typify_block_param(closure=#{closure.inspect}) - starting" } block_pin = closure if block_pin.is_a?(Pin::Block) && block_pin.receiver return block_pin.typify_parameters(api_map)[index] diff --git a/lib/solargraph/pin/reference/override.rb b/lib/solargraph/pin/reference/override.rb index d547e3caf..986168e91 100644 --- a/lib/solargraph/pin/reference/override.rb +++ b/lib/solargraph/pin/reference/override.rb @@ -14,6 +14,10 @@ def closure nil end + def inner_desc + super + ", tags=#{tags.inspect}, delete=#{delete.inspect}" + end + def initialize location, name, tags, delete = [], **splat super(location: location, name: name, **splat) @tags = tags diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 9013dd0d9..f9c3baafa 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -1,5 +1,7 @@ require 'fileutils' require 'rbs' +require 'yard' +require 'yard-activesupport-concern' module Solargraph module PinCache @@ -27,7 +29,10 @@ def work_dir end def yardoc_path gemspec - File.join(base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc") + File.join(base_dir, + "yard-#{YARD::VERSION}", + "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}", + "#{gemspec.name}-#{gemspec.version}.yardoc") end def stdlib_path diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 321ae39e1..344b7c883 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -295,6 +295,7 @@ def global_decl_to_pin decl name: name, closure: closure, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), source: :rbs ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags @@ -414,15 +415,16 @@ def method_def_to_pin decl, closure, context # @return [void] def method_def_to_sigs decl, pin decl.overloads.map do |overload| + type_location = location_decl_to_pin_location(overload.method_type.location) generics = overload.method_type.type_params.map(&:name).map(&:to_s) signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) block = if overload.method_type.block block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin) - Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, - closure: pin, source: :rbs) + Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, + type_location: type_location, closure: pin) end - Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, - closure: pin, source: :rbs) + Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, + type_location: type_location, closure: pin) end end @@ -441,44 +443,47 @@ def location_decl_to_pin_location(location) # @param pin [Pin::Method] # @return [Array(Array, ComplexType)] def parts_of_function type, pin - return [[Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs)], ComplexType.try_parse(method_type_to_tag(type)).force_rooted] if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) + type_location = pin.type_location + return [[Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, type_location: type_location)], ComplexType.try_parse(method_type_to_tag(type)).force_rooted] if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) parameters = [] arg_num = -1 type.type.required_positionals.each do |param| name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs) + parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) end type.type.optional_positionals.each do |param| name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, + type_location: type_location, source: :rbs) end if type.type.rest_positionals name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, source: :rbs) + parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, source: :rbs, type_location: type_location) end type.type.trailing_positionals.each do |param| name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs) + parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs, type_location: type_location) end type.type.required_keywords.each do |orig, param| name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, - source: :rbs) + source: :rbs, type_location: type_location) end type.type.optional_keywords.each do |orig, param| name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, + type_location: type_location, source: :rbs) end if type.type.rest_keywords name = type.type.rest_keywords.name ? type.type.rest_keywords.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, name: type.type.rest_keywords.name.to_s, closure: pin, - source: :rbs) + source: :rbs, type_location: type_location) end rooted_tag = method_type_to_tag(type) @@ -516,9 +521,10 @@ def attr_writer_to_pin(decl, closure, context) final_scope = decl.kind == :instance ? :instance : :class name = "#{decl.name.to_s}=" visibility = calculate_method_visibility(decl, context, closure, final_scope, name) + type_location = location_decl_to_pin_location(decl.location) pin = Solargraph::Pin::Method.new( name: name, - type_location: location_decl_to_pin_location(decl.location), + type_location: type_location, closure: closure, parameters: [], comments: decl.comment&.string, @@ -532,7 +538,8 @@ def attr_writer_to_pin(decl, closure, context) name: 'value', return_type: ComplexType.try_parse(other_type_to_tag(decl.type)).force_rooted, source: :rbs, - closure: pin + closure: pin, + type_location: type_location ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) @@ -572,6 +579,7 @@ def cvar_to_pin(decl, closure) name: name, closure: closure, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), source: :rbs ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags @@ -588,6 +596,7 @@ def civar_to_pin(decl, closure) name: name, closure: closure, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), source: :rbs ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 0d265d773..e802b1fb9 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -25,7 +25,7 @@ def pins loader.add(path: Pathname(FILLS_DIRECTORY)) @pins = conversions.pins @pins.concat RbsMap::CoreFills::ALL - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + processed = ApiMap::Store.new(@pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } @pins.replace processed PinCache.serialize_core @pins @@ -33,10 +33,6 @@ def pins @pins end - def loader - @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) - end - private def loader diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 27d5c4c24..8f02f6ec9 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -137,15 +137,18 @@ def uncache *gems # @param names [Array] # @return [void] def gems *names + api_map = ApiMap.load('.') if names.empty? - Gem::Specification.to_a.each { |spec| do_cache spec } + Gem::Specification.to_a.each { |spec| do_cache spec, api_map } + STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems." else names.each do |name| spec = Gem::Specification.find_by_name(*name.split('=')) - do_cache spec + do_cache spec, api_map rescue Gem::MissingSpecError warn "Gem '#{name}' not found" end + STDERR.puts "Documentation cached for #{names.count} gems." end end @@ -256,8 +259,7 @@ def pin_description pin # @param gemspec [Gem::Specification] # @return [void] - def do_cache gemspec - api_map = ApiMap.load('.') + def do_cache gemspec, api_map # @todo if the rebuild: option is passed as a positional arg, # typecheck doesn't complain on the below line api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 8fdeed228..9eded6b65 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -15,6 +15,7 @@ class Source # class Chain include Equality + include Logging autoload :Link, 'solargraph/source/chain/link' autoload :Call, 'solargraph/source/chain/call' @@ -101,6 +102,7 @@ def base # @return [::Array] Pins representing possible return # types of this method. def define api_map, name_pin, locals + logger.debug { "Chain#define(name_pin=#{name_pin.desc}, links=#{links.map(&:desc)}, locals=#{locals}) - starting" } return [] if undefined? # working_pin is the surrounding closure pin for the link @@ -126,7 +128,9 @@ def define api_map, name_pin, locals 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) + out = links.last.resolve(api_map, working_pin, locals) + logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.desc}, locals=#{locals}) => #{out}" } + out end # @param api_map [ApiMap] @@ -204,6 +208,8 @@ def to_s private + include Logging + # @param pins [::Array] # @param context [Pin::Base] # @param api_map [ApiMap] diff --git a/lib/solargraph/source/chain/array.rb b/lib/solargraph/source/chain/array.rb index 544934d8a..82c793a49 100644 --- a/lib/solargraph/source/chain/array.rb +++ b/lib/solargraph/source/chain/array.rb @@ -29,7 +29,9 @@ def resolve api_map, name_pin, locals else ComplexType::UniqueType.new('Array', [], child_types, rooted: true, parameters_type: :fixed) end - [Pin::ProxyType.anonymous(type, source: :chain)] + out = [Pin::ProxyType.anonymous(type, source: :chain)] + logger.debug { "Array#resolve(self=#{self}) => #{out}" } + out end end end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index cd89a5d85..114fb72d8 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -48,6 +48,7 @@ def with_block? # @param name_pin [Pin::Closure] name_pin.binder should give us the type of the object on which 'word' will be invoked # @param locals [::Array] def resolve api_map, name_pin, locals + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags.inspect}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - starting" } return super_pins(api_map, name_pin) if word == 'super' return yield_pins(api_map, name_pin) if word == 'yield' found = if head? @@ -55,16 +56,34 @@ def resolve api_map, name_pin, locals else [] end - return inferred_pins(found, api_map, name_pin, locals) unless found.empty? + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - found=#{found}" } + unless found.empty? + out = inferred_pins(found, api_map, name_pin, locals) unless found.empty? + logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}) - found=#{found} => #{out}" } + return out + end + # @param [ComplexType::UniqueType] pins = name_pin.binder.each_unique_type.flat_map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) [stack.first].compact end - return [] if pins.empty? - inferred_pins(pins, api_map, name_pin, locals) + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - pins=#{pins.map(&:desc)} - api_map gave pins=#{pins}" } + if pins.empty? + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) => [] - found no pins for #{word} in #{name_pin.binder}" } + return [] + end + out = inferred_pins(pins, api_map, name_pin, locals) + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - pins=#{pins.map(&:desc)} => #{out}" } + out end + def desc + "#{word}(#{arguments.map(&:desc).join(', ')})" + end + + include Logging + private # @param pins [::Enumerable] @@ -87,17 +106,26 @@ def inferred_pins pins, api_map, name_pin, locals sorted_overloads = with_block + without_block # @type [Pin::Signature, nil] new_signature_pin = nil + atypes = [] sorted_overloads.each do |ol| - next unless ol.arity_matches?(arguments, with_block?) + unless ol.arity_matches?(arguments, with_block?) + logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - rejecting #{ol} because arity did not match - arguments=#{arguments} vs parameters=#{ol.parameters}, with_block?=#{with_block?} vs ol.block=#{ol.block}" } + next + end match = true - atypes = [] arguments.each_with_index do |arg, idx| param = ol.parameters[idx] if param.nil? match = ol.parameters.any?(&:restarg?) + if match + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}, ) - accepting rest via restarg - #{ol}" } + else + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - more args than parameters found - #{arg} not matched - #{ol} not matched" } + end break end + logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - resolving arg #{arg.desc}" } atype = atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(name_pin.context, source: :chain), locals) unless param.compatible_arg?(atype, api_map) || param.restarg? match = false @@ -107,6 +135,7 @@ def inferred_pins pins, api_map, name_pin, locals if match if ol.block && with_block? block_atypes = ol.block.parameters.map(&:return_type) + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - ol.block.parameters=#{ol.block.parameters}, block_atypes=#{block_atypes.map(&:desc)}" } 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) @@ -116,6 +145,7 @@ def inferred_pins pins, api_map, name_pin, locals end # @type new_signature_pin [Pin::Signature] new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes, nil, nil, blocktype) + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - resolved generics in #{ol} (#{ol.generics}) to #{new_signature_pin}" } new_return_type = new_signature_pin.return_type if head? # If we're at the head of the chain, we called a @@ -142,7 +172,12 @@ def inferred_pins pins, api_map, name_pin, locals end break if type.defined? end - p = p.with_single_signature(new_signature_pin) unless new_signature_pin.nil? + if new_signature_pin.nil? + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - found no matching signatures for #{p}" } + else + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - accepting signature #{new_signature_pin}" } + p = p.with_single_signature(new_signature_pin) + end next p.proxy(type) if type.defined? if !p.macros.empty? result = process_macro(p, api_map, name_pin.context, locals) @@ -164,6 +199,8 @@ def inferred_pins pins, api_map, name_pin, locals selfy == pin.return_type ? pin : pin.proxy(selfy) end end + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) => #{out}" } + out end # @param pin [Pin::Base] @@ -264,6 +301,7 @@ def super_pins api_map, name_pin # @return [::Array] def yield_pins api_map, name_pin method_pin = find_method_pin(name_pin) + logger.debug { "Call#yield_pins(name_pin=#{name_pin}) - method_pin=#{method_pin.inspect}" } return [] unless method_pin method_pin.signatures.map(&:block).compact.map do |signature_pin| diff --git a/lib/solargraph/source/chain/class_variable.rb b/lib/solargraph/source/chain/class_variable.rb index a804d89e5..dcd8fcdd4 100644 --- a/lib/solargraph/source/chain/class_variable.rb +++ b/lib/solargraph/source/chain/class_variable.rb @@ -5,7 +5,9 @@ class Source class Chain class ClassVariable < Link def resolve api_map, name_pin, locals - api_map.get_class_variable_pins(name_pin.context.namespace).select{|p| p.name == word} + out = api_map.get_class_variable_pins(name_pin.context.namespace).select { |p| p.name == word } + logger.debug { "ClassVariable#resolve(word=#{word.inspect}, name_pin=#{name_pin.inspect}, name_pin.scope=#{name_pin.scope}, name_pin.context=#{name_pin.context}, name_pin.context.namespace=#{name_pin.context.namespace} => #{out}" } + out end end end diff --git a/lib/solargraph/source/chain/constant.rb b/lib/solargraph/source/chain/constant.rb index 58b274aeb..cdb96bf1d 100644 --- a/lib/solargraph/source/chain/constant.rb +++ b/lib/solargraph/source/chain/constant.rb @@ -17,6 +17,7 @@ def resolve api_map, name_pin, locals base = word gates = crawl_gates(name_pin) end + logger.debug { "Constant#resolve(word=#{word.inspect}) - gates=#{gates}, name_pin=#{name_pin}" } parts = base.split('::') gates.each do |gate| # @todo 'Wrong argument type for @@ -33,6 +34,7 @@ def resolve api_map, name_pin, locals break if type.undefined? end next if type.undefined? + logger.debug { "Constant#resolve(word=#{word.inspect}) - name_pin=#{name_pin}, type=#{type}, type.namespace=#{type.namespace}" } result = api_map.get_constants('', type.namespace).select { |pin| pin.name == parts.last } return result unless result.empty? end @@ -49,6 +51,7 @@ def crawl_gates pin if clos.is_a?(Pin::Namespace) gates = clos.gates gates.push('') if gates.empty? + logger.debug { "Constant#crawl_gates(pin=#{pin}) clos=#{clos}, clos.path=#{clos.path.inspect} - gates=#{gates}" } return gates end clos = clos.closure diff --git a/lib/solargraph/source/chain/z_super.rb b/lib/solargraph/source/chain/z_super.rb index 5b0106c92..1d6e902b0 100644 --- a/lib/solargraph/source/chain/z_super.rb +++ b/lib/solargraph/source/chain/z_super.rb @@ -22,8 +22,12 @@ def initialize word, with_block = false # @param name_pin [Pin::Base] # @param locals [::Array] def resolve api_map, name_pin, locals - return super_pins(api_map, name_pin) + pins = super_pins(api_map, name_pin) + logger.debug { "ZSuper#resolve(#{word.inspect}, name_pin=#{name_pin.inspect}) => #{pins}" } + pins end + + include Logging end end end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 84b3a4bcc..854ffa223 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -129,7 +129,9 @@ def references name def locals_at(location) return [] if location.filename != filename closure = locate_named_path_pin(location.range.start.line, location.range.start.character) - locals.select { |pin| pin.visible_at?(closure, location) } + out = locals.select { |pin| pin.visible_at?(closure, location) } + logger.debug { "SourceMap#locals_at(#{location.inspect}) => #{out.map(&:inspect)}" } + out end class << self @@ -196,5 +198,7 @@ def _locate_pin line, character, *klasses # Assuming the root pin is always valid found || pins.first end + + include Logging end end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index ba69b1b93..a507182c3 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -32,12 +32,19 @@ def types # @return [Completion] def complete + logger.debug { "Clip#complete() - #{cursor.word}" } 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 == '' - return Completion.new([], cursor.range) if cursor.chain.literal? + if cursor.chain.literal? + out = Completion.new([], cursor.range) + logger.debug { "Clip#complete() => #{out} - literal" } + return out + end if cursor.comment? + logger.debug { "Clip#complete() => #{tag_complete} - comment" } tag_complete else + logger.debug { "Clip#complete() => #{code_complete.inspect} - !comment" } code_complete end end @@ -90,6 +97,8 @@ def translate phrase chain.define(api_map, block, locals) end + include Logging + private # @return [ApiMap] @@ -176,6 +185,7 @@ def tag_complete # @return [Completion] def code_complete + logger.debug { "Clip#code_complete() start - #{cursor.word}" } result = [] result.concat complete_keyword_parameters if cursor.chain.constant? || cursor.start_of_constant? @@ -191,6 +201,7 @@ def code_complete ComplexType::UNDEFINED end end + logger.debug { "Clip#code_complete() - type=#{type}" } if type.undefined? if full.include?('::') result.concat api_map.get_constants(full, *gates) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index aa215f97b..cde4b8725 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -267,6 +267,7 @@ def call_problems break if found missing = base base = base.base + logger.debug { "TypeChecker#call_problems: found=#{found}, base=#{base}, missing=#{missing}" } end closest = found.typify(api_map) if found # @todo remove the internal_or_core? check at a higher-than-strict level @@ -695,5 +696,7 @@ def without_ignored problems node && source_map.source.comments_for(node)&.include?('@sg-ignore') end end + + include Logging end end diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index 0f307b521..91c07b004 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = '0.55.4' + VERSION = '0.56.alpha' end diff --git a/lib/solargraph/yard_map/helpers.rb b/lib/solargraph/yard_map/helpers.rb index 71b047df1..7058af9c3 100644 --- a/lib/solargraph/yard_map/helpers.rb +++ b/lib/solargraph/yard_map/helpers.rb @@ -7,10 +7,36 @@ module Helpers # @param spec [Gem::Specification, nil] # @return [Solargraph::Location, nil] def object_location code_object, spec - return nil if spec.nil? || code_object.nil? || code_object.file.nil? || code_object.line.nil? + if spec.nil? || code_object.nil? || code_object.file.nil? || code_object.line.nil? + if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject) + # If the code object is a namespace, use the namespace's location + return object_location(code_object.namespace, spec) + end + return Solargraph::Location.new(__FILE__, Solargraph::Range.from_to(__LINE__ - 1, 0, __LINE__ - 1, 0)) + end file = File.join(spec.full_gem_path, code_object.file) Solargraph::Location.new(file, Solargraph::Range.from_to(code_object.line - 1, 0, code_object.line - 1, 0)) end + + # @param spec [Gem::Specification, nil] + def create_closure_namespace_for(code_object, spec) + code_object_for_location = code_object + # code_object.namespace is sometimes a YARD proxy object pointing to a method path ("Object#new") + code_object_for_location = code_object.namespace if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject) + namespace_location = object_location(code_object_for_location, spec) + ns_name = code_object.namespace.to_s + if ns_name.empty? + Solargraph::Pin::ROOT_PIN + else + Solargraph::Pin::Namespace.new( + name: ns_name, + closure: Pin::ROOT_PIN, + gates: [code_object.namespace.to_s], + source: :yardoc, + location: namespace_location + ) + end + end end end end diff --git a/lib/solargraph/yard_map/mapper/to_constant.rb b/lib/solargraph/yard_map/mapper/to_constant.rb index 65f271e44..0c2ac6dd2 100644 --- a/lib/solargraph/yard_map/mapper/to_constant.rb +++ b/lib/solargraph/yard_map/mapper/to_constant.rb @@ -8,11 +8,8 @@ module ToConstant # @param code_object [YARD::CodeObjects::Base] def self.make code_object, closure = nil, spec = nil - closure ||= Solargraph::Pin::Namespace.new( - name: code_object.namespace.to_s, - gates: [code_object.namespace.to_s], - source: :yardoc, - ) + closure ||= create_closure_namespace_for(code_object, spec) + Pin::Constant.new( location: object_location(code_object, spec), closure: closure, diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index 82e090e52..df431bb3c 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -19,12 +19,7 @@ module ToMethod # @param spec [Gem::Specification, nil] # @return [Solargraph::Pin::Method] def self.make code_object, name = nil, scope = nil, visibility = nil, closure = nil, spec = nil - closure ||= Solargraph::Pin::Namespace.new( - name: code_object.namespace.to_s, - gates: [code_object.namespace.to_s], - type: code_object.namespace.is_a?(YARD::CodeObjects::ClassObject) ? :class : :module, - source: :yardoc, - ) + closure ||= create_closure_namespace_for(code_object, spec) location = object_location(code_object, spec) name ||= code_object.name.to_s return_type = ComplexType::SELF if name == 'new' diff --git a/lib/solargraph/yard_map/mapper/to_namespace.rb b/lib/solargraph/yard_map/mapper/to_namespace.rb index b721324cb..8dfb818a6 100644 --- a/lib/solargraph/yard_map/mapper/to_namespace.rb +++ b/lib/solargraph/yard_map/mapper/to_namespace.rb @@ -8,14 +8,11 @@ module ToNamespace # @param code_object [YARD::CodeObjects::NamespaceObject] def self.make code_object, spec, closure = nil - closure ||= Solargraph::Pin::Namespace.new( - name: code_object.namespace.to_s, - closure: Pin::ROOT_PIN, - gates: [code_object.namespace.to_s], - source: :yardoc, - ) + closure ||= create_closure_namespace_for(code_object, spec) + location = object_location(code_object, spec) + Pin::Namespace.new( - location: object_location(code_object, spec), + location: location, name: code_object.name.to_s, comments: code_object.docstring ? code_object.docstring.all.to_s : '', type: code_object.is_a?(YARD::CodeObjects::ClassObject) ? :class : :module, diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 797413230..c9fa16bbe 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -17,7 +17,7 @@ def cache(gemspec) Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" Dir.chdir gemspec.gem_dir do - `yardoc --db #{path} --no-output --plugin solargraph` + `yardoc --db #{path} --no-output --plugin solargraph --plugin activesupport-concern` end path end diff --git a/solargraph.gemspec b/solargraph.gemspec index 5008b6247..978f59c28 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -42,6 +42,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'tilt', '~> 2.0' s.add_runtime_dependency 'yard', '~> 0.9', '>= 0.9.24' s.add_runtime_dependency 'yard-solargraph', '~> 0.1' + s.add_runtime_dependency 'yard-activesupport-concern', '~> 0.0' s.add_development_dependency 'pry', '~> 0.15' s.add_development_dependency 'public_suffix', '~> 3.1' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index faba8172e..d6d9ac3b1 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ end require 'solargraph' # Suppress logger output in specs (if possible) -Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) +Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') def with_env_var(name, value) old_value = ENV[name] # Store the old value