From 821024c5254c2f98920d4ffc8df50e1b722a568b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Apr 2025 12:29:41 -0400 Subject: [PATCH 001/116] Document a log level env variable --- README.md | 4 ++++ lib/solargraph/logging.rb | 10 ++++++++-- lib/solargraph/source/chain.rb | 6 +++++- spec/spec_helper.rb | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 30edafcf0..b1630773c 100755 --- a/README.md +++ b/README.md @@ -122,6 +122,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/logging.rb b/lib/solargraph/logging.rb index 4dce90f77..d74a08b0a 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -11,8 +11,14 @@ 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) @@logger.formatter = proc do |severity, datetime, progname, msg| "[#{severity}] #{msg}\n" end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 556da1718..a49406865 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -9,6 +9,8 @@ class Source # values. # class Chain + include Logging + autoload :Link, 'solargraph/source/chain/link' autoload :Call, 'solargraph/source/chain/call' autoload :QCall, 'solargraph/source/chain/q_call' @@ -108,7 +110,9 @@ def infer_uncached api_map, name_pin, locals end pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) - maybe_nil(type) + out = maybe_nil(type) + logging.logger.debug { "Chain#infer_uncached(links=#{self.links} => #{out}" } + out end # @return [Boolean] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5e0385b74..4710c6534 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,4 +7,4 @@ 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_DEBUG_LEVEL') From 772217c444d2aadc55ff9c60ecd0c2265fbdf425 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Apr 2025 12:36:28 -0400 Subject: [PATCH 002/116] Fix logger reference --- lib/solargraph/source/chain.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index a49406865..42fb506ea 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -111,7 +111,7 @@ def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) - logging.logger.debug { "Chain#infer_uncached(links=#{self.links} => #{out}" } + logger.debug { "Chain#infer_uncached(links=#{self.links} => #{out}" } out end From c15e15726e37070e51bd9e2dfe29010795110269 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Apr 2025 15:19:36 -0400 Subject: [PATCH 003/116] Fix env var name --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4710c6534..a0d99cb88 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,4 +7,4 @@ end require 'solargraph' # Suppress logger output in specs (if possible) -Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_DEBUG_LEVEL') +Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') From 87c7e4f6b8d55b241742d0430767036f2864f3c5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 18 Apr 2025 10:06:04 -0400 Subject: [PATCH 004/116] Allow log level to be overridden per file Useful for debug-level logging being turned on selectively at dev time --- lib/solargraph/logging.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index 4dce90f77..e3173e78c 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -16,12 +16,29 @@ module Logging @@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 + @@logger.level + end + # @return [Logger] def logger - @@logger + @logger ||= if log_level == @@logger.level + @@logger + else + logger = Logger.new(STDERR, log_level) + logger.formatter = @@logger.formatter + logger + end end end end From f2baf0102e149b25f748677410c542b0b91ff867 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 18 Apr 2025 10:51:50 -0400 Subject: [PATCH 005/116] Various debugging-related log statements --- lib/solargraph/api_map.rb | 9 ++++++- lib/solargraph/complex_type.rb | 7 +++++- lib/solargraph/complex_type/unique_type.rb | 6 ++++- lib/solargraph/pin/block.rb | 18 +++++++++++--- lib/solargraph/pin/closure.rb | 2 ++ lib/solargraph/pin/method.rb | 13 ++++++++-- lib/solargraph/pin/parameter.rb | 4 ++++ lib/solargraph/source/chain.rb | 21 +++++++++++++--- lib/solargraph/source/chain/call.rb | 28 ++++++++++++++++++---- 9 files changed, 93 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 7b40ac256..8ad165d69 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -612,7 +612,9 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first methods = if namespace_pin && rooted_tag != fqns methods = raw_methods.map do |method_pin| - method_pin.resolve_generics(namespace_pin, rooted_type) + out = method_pin.resolve_generics(namespace_pin, rooted_type) + logger.debug { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag}) - resolved generics on #{method_pin} to #{out} from #{namespace_pin} and #{rooted_type}}" } if method_pin.name == '[]' + out end else raw_methods @@ -808,5 +810,10 @@ def resolve_method_alias pin } Pin::Method.new **args end + + private + + include Logging + end end diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index cbfcc7afe..08e521466 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -22,12 +22,15 @@ def initialize types = [UniqueType::UNDEFINED] # @param context [String] # @return [ComplexType] def qualify api_map, context = '' + logger.debug { "ComplexType#qualify(self=#{self}, context=#{context.inspect}) - starting" } red = reduce_object types = red.items.map do |t| next t if ['Boolean', 'nil', 'void', 'undefined'].include?(t.name) t.qualify api_map, context end - ComplexType.new(types).reduce_object + out = ComplexType.new(types).reduce_object + logger.debug { "ComplexType#qualify(self=#{self}, context=#{context.inspect}) => #{out.rooted_tags}" } + out end # @param generics_to_resolve [Enumerable]] @@ -326,6 +329,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 6dfd9d2b0..f1c03054e 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -207,7 +207,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 idx = definitions.generics.index(t.subtypes.first&.name) next t if idx.nil? @@ -216,6 +216,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] @@ -298,6 +300,8 @@ def selfy? UNDEFINED = UniqueType.new('undefined', rooted: false) BOOLEAN = UniqueType.new('Boolean', rooted: true) + + include Logging end end end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 57239144f..a658c2e35 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -67,10 +67,13 @@ 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? @@ -85,15 +88,24 @@ def typify_parameters(api_map) 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(chain.base.infer(api_map, self, locals).namespace).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 } + out = parameters.map { ComplexType::UNDEFINED } + logger.debug { "Block#typify_parameters() => #{out.map(&:rooted_tags)}" } + out end private diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 0b8645355..753534f37 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -47,6 +47,8 @@ def generics_as_rbs generics.join(', ') + ' ' end + + include Logging end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 354c0944d..84d6b902d 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -192,10 +192,19 @@ def path end def typify api_map + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}, return_type=#{return_type.rooted_tags}) - starting" } decl = super - return decl unless decl.undefined? + unless decl.undefined? + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl}" } + return decl + end type = see_reference(api_map) || typify_from_super(api_map) - return type.qualify(api_map, namespace) unless type.nil? + logger.debug { "Method#typify(self=#{self}) - type=#{type}" } + unless type.nil? + qualified = type.qualify(api_map, namespace) + logger.debug { "Method#typify(self=#{self}) => #{qualified}" } + return qualified + end name.end_with?('?') ? ComplexType::BOOLEAN : ComplexType::UNDEFINED end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ce7133899..b94c9c5af 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -107,6 +107,7 @@ def index # @param api_map [ApiMap] def typify api_map + logger.debug { "Parameter#typify(closure=#{closure.inspect}) - starting" } 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) end @@ -122,6 +123,8 @@ def try_merge! pin true end + include Logging + private # @return [YARD::Tags::Tag, nil] @@ -142,6 +145,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" } if closure.is_a?(Pin::Block) && closure.receiver return closure.typify_parameters(api_map)[index] end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 556da1718..4b819445c 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -67,16 +67,23 @@ def base # # @return [::Array] def define api_map, name_pin, locals + logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) - starting" } return [] if undefined? working_pin = name_pin links[0..-2].each do |link| pins = link.resolve(api_map, working_pin, locals) type = infer_first_defined(pins, working_pin, api_map, locals) - return [] if type.undefined? + 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 working_pin = Pin::ProxyType.anonymous(type) + 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 = name_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.inspect}, locals=#{locals}) => #{out}" } + out end # @param api_map [ApiMap] @@ -108,7 +115,9 @@ def infer_uncached api_map, name_pin, locals end pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) - maybe_nil(type) + out = maybe_nil(type) + logger.debug { "Chain#infer_uncached(links=#{self.links} => #{out}" } + out end # @return [Boolean] @@ -137,8 +146,14 @@ def nullable? links.any?(&:nullable?) end + def desc + links.map(&:desc).to_s + end + private + include Logging + # @param pins [::Array] # @param context [Pin::Base] # @param api_map [ApiMap] diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 5a76ab094..86c34bc77 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -31,6 +31,7 @@ def with_block? # @param name_pin [Pin::Base] # @param locals [::Array] def resolve api_map, name_pin, locals + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, 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? @@ -38,15 +39,30 @@ def resolve api_map, name_pin, locals else [] end - return inferred_pins(found, api_map, name_pin.context, locals) unless found.empty? + unless found.empty? + out = inferred_pins(found, api_map, name_pin.context, locals) + 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| api_map.get_method_stack(context.namespace == '' ? '' : context.tag, word, scope: context.scope) end - return [] if pins.empty? - inferred_pins(pins, api_map, name_pin.context, locals) + if pins.empty? + logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } + return [] + end + out = inferred_pins(pins, api_map, name_pin.context, locals) + logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}) - pins=#{pins} => #{out}" } + out + end + + def desc + "#{word}(#{arguments.map(&:desc).join(', ')})" end + include Logging + private # @param pins [::Enumerable] @@ -78,6 +94,8 @@ def inferred_pins pins, api_map, context, locals match = ol.parameters.any?(&:restarg?) 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(context), locals) # @todo Weak type comparison # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) @@ -109,7 +127,7 @@ def inferred_pins pins, api_map, context, locals end p end - result.map do |pin| + out = result.map do |pin| if pin.path == 'Class#new' && context.tag != 'Class' pin.proxy(ComplexType.try_parse(context.namespace)) else @@ -118,6 +136,8 @@ def inferred_pins pins, api_map, context, locals selfy == pin.return_type ? pin : pin.proxy(selfy) end end + logger.debug { "Call#inferred_pins(pins=#{pins}, name_pin=#{name_pin}) => #{out}" } + out end # @param pin [Pin::Base] From 83e133d9a7ee64f7945e6ceca6acdddb3c46380e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 19 Apr 2025 07:34:52 -0400 Subject: [PATCH 006/116] Various debugging-related log statements --- lib/solargraph/complex_type.rb | 6 ++++-- lib/solargraph/source/chain.rb | 2 +- lib/solargraph/source/chain/call.rb | 28 ++++++++++++++++++++------ lib/solargraph/source/chain/z_super.rb | 6 +++++- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 08e521466..71e8f616e 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -22,7 +22,7 @@ def initialize types = [UniqueType::UNDEFINED] # @param context [String] # @return [ComplexType] def qualify api_map, context = '' - logger.debug { "ComplexType#qualify(self=#{self}, context=#{context.inspect}) - starting" } + logger.debug { "ComplexType#qualify(self=#{self.rooted_tags}, context=#{context.inspect}) - starting" } red = reduce_object types = red.items.map do |t| next t if ['Boolean', 'nil', 'void', 'undefined'].include?(t.name) @@ -174,7 +174,9 @@ def force_rooted # @return [ComplexType] def resolve_generics definitions, context_type result = @items.map { |i| i.resolve_generics(definitions, context_type) } - ComplexType.try_parse(*result.map(&:tag)) + out = ComplexType.try_parse(*result.map(&:tag)) + logger.debug { "ComplexType#resolve_generics(self=#{rooted_tags}, definitions=#{definitions}, context_type=#{context_type.rooted_tags} => #{out.rooted_tags}" } + out end # @param dst [String] diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 4b819445c..31f133a04 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -116,7 +116,7 @@ def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) - logger.debug { "Chain#infer_uncached(links=#{self.links} => #{out}" } + logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)} => #{out}" } out end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 86c34bc77..d46da3f13 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -49,11 +49,11 @@ def resolve api_map, name_pin, locals api_map.get_method_stack(context.namespace == '' ? '' : context.tag, word, scope: context.scope) end if pins.empty? - logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } return [] end out = inferred_pins(pins, api_map, name_pin.context, locals) - logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}) - pins=#{pins} => #{out}" } + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, name_pin=#{name_pin}) - pins=#{pins.map(&:desc)} => #{out}" } out end @@ -83,15 +83,23 @@ def inferred_pins pins, api_map, context, locals sorted_overloads = overloads.sort { |ol| ol.block? ? -1 : 1 } new_signature_pin = nil + atypes = [] sorted_overloads.each do |ol| - next unless arity_matches?(arguments, ol) + unless arity_matches?(arguments, ol) + 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}" } + 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(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - accepting rest via restarg - #{ol}" } + else + logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - more args than parameters found - #{arg} not matched - #{ol} not matched" } + end break end @@ -100,6 +108,7 @@ def inferred_pins pins, api_map, context, locals # @todo Weak type comparison # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) unless param.return_type.undefined? || atype.name == param.return_type.name || api_map.super_and_sub?(param.return_type.name, atype.name) || param.return_type.generic? + logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - rejecting signature #{ol}" } match = false break end @@ -116,7 +125,12 @@ def inferred_pins pins, api_map, context, 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(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - found no matching signatures for #{p}" } + else + logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - 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, context, locals) @@ -127,6 +141,7 @@ def inferred_pins pins, api_map, context, locals end p end + logger.debug { "Call#inferred_pins(pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) - result=#{result}" } out = result.map do |pin| if pin.path == 'Class#new' && context.tag != 'Class' pin.proxy(ComplexType.try_parse(context.namespace)) @@ -136,7 +151,7 @@ def inferred_pins pins, api_map, context, locals selfy == pin.return_type ? pin : pin.proxy(selfy) end end - logger.debug { "Call#inferred_pins(pins=#{pins}, name_pin=#{name_pin}) => #{out}" } + logger.debug { "Call#inferred_pins(pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) => #{out}" } out end @@ -238,6 +253,7 @@ def super_pins api_map, name_pin # @return [::Array] def yield_pins api_map, name_pin method_pin = api_map.get_method_stack(name_pin.namespace, name_pin.name, scope: name_pin.context.scope).first + logger.debug { "Call#yield_pins(name_pin=#{name_pin}) - method_pin=#{method_pin.inspect}" } return [] if method_pin.nil? method_pin.signatures.map(&:block).compact diff --git a/lib/solargraph/source/chain/z_super.rb b/lib/solargraph/source/chain/z_super.rb index 650ea6039..ec3150641 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 From 1074b60206b0e410cf4a29d96e372df4ebc67f86 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 16 Apr 2025 11:35:55 -0400 Subject: [PATCH 007/116] Populate location information from RBS files (#768) * Populate location information from RBS files The 'rbs' gem maps the location of different definitions to the relevant point in the RGS files themselves - this change provides the ability to jump into the right place in those files to see the type definition via the LSP. * Prefer source location in language server * Resolve merge issue * Fix Path vs String type error --- .../message/text_document/definition.rb | 6 ++-- .../message/text_document/document_symbol.rb | 6 ++-- .../message/text_document/type_definition.rb | 6 ++-- .../message/workspace/workspace_symbol.rb | 4 +-- lib/solargraph/pin/base.rb | 12 ++++++- lib/solargraph/rbs_map/conversions.rb | 32 ++++++++++++++----- 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/lib/solargraph/language_server/message/text_document/definition.rb b/lib/solargraph/language_server/message/text_document/definition.rb index 99d908652..47bf7a60d 100644 --- a/lib/solargraph/language_server/message/text_document/definition.rb +++ b/lib/solargraph/language_server/message/text_document/definition.rb @@ -13,10 +13,10 @@ def process def code_location suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column) return nil if suggestions.empty? - suggestions.reject { |pin| pin.location.nil? || pin.location.filename.nil? }.map do |pin| + suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| { - uri: file_to_uri(pin.location.filename), - range: pin.location.range.to_hash + uri: file_to_uri(pin.best_location.filename), + range: pin.best_location.range.to_hash } end end diff --git a/lib/solargraph/language_server/message/text_document/document_symbol.rb b/lib/solargraph/language_server/message/text_document/document_symbol.rb index 19a64cf93..2490f5c6d 100644 --- a/lib/solargraph/language_server/message/text_document/document_symbol.rb +++ b/lib/solargraph/language_server/message/text_document/document_symbol.rb @@ -6,15 +6,15 @@ class Solargraph::LanguageServer::Message::TextDocument::DocumentSymbol < Solarg def process pins = host.document_symbols params['textDocument']['uri'] info = pins.map do |pin| - next nil unless pin.location&.filename + next nil unless pin.best_location&.filename result = { name: pin.name, containerName: pin.namespace, kind: pin.symbol_kind, location: { - uri: file_to_uri(pin.location.filename), - range: pin.location.range.to_hash + uri: file_to_uri(pin.best_location.filename), + range: pin.best_location.range.to_hash }, deprecated: pin.deprecated? } diff --git a/lib/solargraph/language_server/message/text_document/type_definition.rb b/lib/solargraph/language_server/message/text_document/type_definition.rb index feb5dfdce..8143d7710 100644 --- a/lib/solargraph/language_server/message/text_document/type_definition.rb +++ b/lib/solargraph/language_server/message/text_document/type_definition.rb @@ -13,10 +13,10 @@ def process def code_location suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column) return nil if suggestions.empty? - suggestions.reject { |pin| pin.location.nil? || pin.location.filename.nil? }.map do |pin| + suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin| { - uri: file_to_uri(pin.location.filename), - range: pin.location.range.to_hash + uri: file_to_uri(pin.best_location.filename), + range: pin.best_location.range.to_hash } end end diff --git a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb index ab1c1248f..780e4aa0b 100644 --- a/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +++ b/lib/solargraph/language_server/message/workspace/workspace_symbol.rb @@ -6,14 +6,14 @@ class Solargraph::LanguageServer::Message::Workspace::WorkspaceSymbol < Solargra def process pins = host.query_symbols(params['query']) info = pins.map do |pin| - uri = file_to_uri(pin.location.filename) + uri = file_to_uri(pin.best_location.filename) { name: pin.path, containerName: pin.namespace, kind: pin.symbol_kind, location: { uri: uri, - range: pin.location.range.to_hash + range: pin.best_location.range.to_hash }, deprecated: pin.deprecated? } diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 455e370f1..13b2e4c15 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -15,6 +15,9 @@ class Base # @return [Solargraph::Location] attr_reader :location + # @return [Solargraph::Location] + attr_reader :type_location + # @return [String] attr_reader :name @@ -25,11 +28,13 @@ class Base attr_accessor :source # @param location [Solargraph::Location, nil] + # @param type_location [Solargraph::Location, nil] # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] # @param comments [String] - def initialize location: nil, closure: nil, name: '', comments: '' + def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '' @location = location + @type_location = type_location @closure = closure @name = name @comments = comments @@ -102,6 +107,11 @@ def variable? false end + # @return [Location, nil] + def best_location + location || type_location + end + # Pin equality is determined using the #nearly? method and also # requiring both pins to have the same location. # diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 6388572f5..9737c2eba 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -82,7 +82,7 @@ def convert_self_types_to_pins decl, module_pin def convert_self_type_to_pins decl, closure include_pin = Solargraph::Pin::Reference::Include.new( name: decl.name.relative!.to_s, - location: rbs_location_to_location(decl.location), + type_location: location_decl_to_pin_location(decl.location), closure: closure ) pins.push include_pin @@ -144,6 +144,7 @@ def class_decl_to_pin decl name: decl.name.relative!.to_s, closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), # @todo some type parameters in core/stdlib have default # values; Solargraph doesn't support that yet as so these # get treated as undefined if not specified @@ -152,6 +153,7 @@ def class_decl_to_pin decl pins.push class_pin if decl.super_class pins.push Solargraph::Pin::Reference::Superclass.new( + type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, name: decl.super_class.name.relative!.to_s ) @@ -166,6 +168,7 @@ def class_decl_to_pin decl def interface_decl_to_pin decl, closure class_pin = Solargraph::Pin::Namespace.new( type: :module, + type_location: location_decl_to_pin_location(decl.location), name: decl.name.relative!.to_s, closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, @@ -185,6 +188,7 @@ def module_decl_to_pin decl module_pin = Solargraph::Pin::Namespace.new( type: :module, name: decl.name.relative!.to_s, + type_location: location_decl_to_pin_location(decl.location), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, generics: decl.type_params.map(&:name).map(&:to_s), @@ -199,10 +203,12 @@ def module_decl_to_pin decl # @param name [String] # @param tag [String] # @param comments [String] + # @param decl [RBS::AST::Declarations::ClassAlias, RBS::AST::Declarations::Constant, RBS::AST::Declarations::ModuleAlias] # @param base [String, nil] Optional conversion of tag to base # # @return [Solargraph::Pin::Constant] - def create_constant(name, tag, comments, base = nil) + def create_constant(name, tag, comments, decl, base = nil) + comments = decl.comment&.string parts = name.split('::') if parts.length > 1 name = parts.last @@ -214,6 +220,7 @@ def create_constant(name, tag, comments, base = nil) constant_pin = Solargraph::Pin::Constant.new( name: name, closure: closure, + type_location: location_decl_to_pin_location(decl.location), comments: comments ) tag = "#{base}<#{tag}>" if base @@ -228,7 +235,7 @@ def class_alias_decl_to_pin decl new_name = decl.new_name.relative!.to_s old_name = decl.old_name.relative!.to_s - pins.push create_constant(new_name, old_name, decl.comment&.string, 'Class') + pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Class') end # @param decl [RBS::AST::Declarations::ModuleAlias] @@ -238,14 +245,14 @@ def module_alias_decl_to_pin decl new_name = decl.new_name.relative!.to_s old_name = decl.old_name.relative!.to_s - pins.push create_constant(new_name, old_name, decl.comment&.string, 'Module') + pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Module') end # @param decl [RBS::AST::Declarations::Constant] # @return [void] def constant_decl_to_pin decl tag = other_type_to_tag(decl.type) - pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string) + pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string, decl) end # @param decl [RBS::AST::Declarations::Global] @@ -276,6 +283,7 @@ def method_def_to_pin decl, closure pin = Solargraph::Pin::Method.new( name: decl.name.to_s, closure: closure, + type_location: location_decl_to_pin_location(decl.location), comments: decl.comment&.string, scope: :instance, signatures: [], @@ -295,6 +303,7 @@ def method_def_to_pin decl, closure name: decl.name.to_s, closure: closure, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), scope: :class, signatures: [], generics: generics @@ -321,13 +330,13 @@ def method_def_to_sigs decl, pin # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] - def rbs_location_to_location(location) + def location_decl_to_pin_location(location) return nil if location&.name.nil? start_pos = Position.new(location.start_line - 1, location.start_column) end_pos = Position.new(location.end_line - 1, location.end_column) range = Range.new(start_pos, end_pos) - Location.new(location.name, range) + Location.new(location.name.to_s, range) end # @param type [RBS::MethodType,RBS::Types::Block] @@ -379,6 +388,7 @@ def parts_of_function type, pin def attr_reader_to_pin(decl, closure) pin = Solargraph::Pin::Method.new( name: decl.name.to_s, + type_location: location_decl_to_pin_location(decl.location), closure: closure, comments: decl.comment&.string, scope: :instance, @@ -394,6 +404,7 @@ def attr_reader_to_pin(decl, closure) def attr_writer_to_pin(decl, closure) pin = Solargraph::Pin::Method.new( name: "#{decl.name.to_s}=", + type_location: location_decl_to_pin_location(decl.location), closure: closure, comments: decl.comment&.string, scope: :instance, @@ -418,6 +429,7 @@ def ivar_to_pin(decl, closure) pin = Solargraph::Pin::InstanceVariable.new( name: decl.name.to_s, closure: closure, + type_location: location_decl_to_pin_location(decl.location), comments: decl.comment&.string ) pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', other_type_to_tag(decl.type))) @@ -460,6 +472,7 @@ def include_to_pin decl, closure generic_values = type.all_params.map(&:to_s) pins.push Solargraph::Pin::Reference::Include.new( name: decl.name.relative!.to_s, + type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure ) @@ -471,6 +484,7 @@ def include_to_pin decl, closure def prepend_to_pin decl, closure pins.push Solargraph::Pin::Reference::Prepend.new( name: decl.name.relative!.to_s, + type_location: location_decl_to_pin_location(decl.location), closure: closure ) end @@ -481,6 +495,7 @@ def prepend_to_pin decl, closure def extend_to_pin decl, closure pins.push Solargraph::Pin::Reference::Extend.new( name: decl.name.relative!.to_s, + type_location: location_decl_to_pin_location(decl.location), closure: closure ) end @@ -491,6 +506,7 @@ def extend_to_pin decl, closure def alias_to_pin decl, closure pins.push Solargraph::Pin::MethodAlias.new( name: decl.new_name.to_s, + type_location: location_decl_to_pin_location(decl.location), original: decl.old_name.to_s, closure: closure ) @@ -597,7 +613,7 @@ def add_mixins decl, namespace klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend pins.push klass.new( name: mixin.name.relative!.to_s, - location: rbs_location_to_location(mixin.location), + location: location_decl_to_pin_location(mixin.location), closure: namespace ) end From 020a7e92a9e9704f7166388c20d42251362eacf7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 16 Apr 2025 12:30:11 -0400 Subject: [PATCH 008/116] Consolidate parameter handling into Pin::Callable (#844) * Consolidate parameter handling into Pin::Closure * Clarify clobbered variable names * Fix bug in to_rbs, add spec, then fix new bug found after running spec * Catch one more Signature.new to translate from strict typechecking * Introduce Pin::Callable * Introduce Pin::Callable * Introduce Pin::Callable * Introduce Pin::Callable * Introduce Pin::Callable * Introduce Pin::Callable * Introduce Pin::Callable * Use Pin::Callable type in args_node.rb * Select String#each_line overload with mandatory vs optional arg info --- lib/solargraph/complex_type.rb | 13 +- .../parser_gem/node_processors/args_node.rb | 42 ++--- lib/solargraph/pin.rb | 1 + lib/solargraph/pin/block.rb | 22 +-- lib/solargraph/pin/callable.rb | 147 ++++++++++++++++++ lib/solargraph/pin/closure.rb | 9 +- lib/solargraph/pin/method.rb | 40 ++--- lib/solargraph/pin/namespace.rb | 2 +- lib/solargraph/pin/parameter.rb | 4 + lib/solargraph/pin/signature.rb | 136 +--------------- lib/solargraph/rbs_map/conversions.rb | 12 +- lib/solargraph/source/chain/call.rb | 15 +- spec/pin/namespace_spec.rb | 1 + spec/source_map/clip_spec.rb | 37 +++++ 14 files changed, 258 insertions(+), 223 deletions(-) create mode 100644 lib/solargraph/pin/callable.rb diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index cbfcc7afe..7f1e81b09 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -221,10 +221,15 @@ class << self # # @param *strings [Array] The type definitions to parse # @return [ComplexType] - # @overload parse(*strings, partial: false) - # @todo Need ability to use a literal true as a type below - # @param partial [Boolean] True if the string is part of a another type - # @return [Array] + # # @overload parse(*strings, partial: false) + # # @todo Need ability to use a literal true as a type below + # # @param partial [Boolean] True if the string is part of a another type + # # @return [Array] + # @sg-ignore + # @todo To be able to select the right signature above, + # Chain::Call needs to know the decl type (:arg, :optarg, + # :kwarg, etc) of the arguments given, instead of just having + # an array of Chains as the arguments. def parse *strings, partial: false # @type [Hash{Array => ComplexType}] @cache ||= {} diff --git a/lib/solargraph/parser/parser_gem/node_processors/args_node.rb b/lib/solargraph/parser/parser_gem/node_processors/args_node.rb index 7f6006111..ce9d77241 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/args_node.rb @@ -6,22 +6,25 @@ module ParserGem module NodeProcessors class ArgsNode < Parser::NodeProcessor::Base def process - if node.type == :forward_args - forward - else - node.children.each do |u| - loc = get_node_location(u) - locals.push Solargraph::Pin::Parameter.new( - location: loc, - closure: region.closure, - comments: comments_for(node), - name: u.children[0].to_s, - assignment: u.children[1], - asgn_code: u.children[1] ? region.code_for(u.children[1]) : nil, - presence: region.closure.location.range, - decl: get_decl(u) - ) - region.closure.parameters.push locals.last + callable = region.closure + if callable.is_a? Pin::Callable + if node.type == :forward_args + forward(callable) + else + node.children.each do |u| + loc = get_node_location(u) + locals.push Solargraph::Pin::Parameter.new( + location: loc, + closure: callable, + comments: comments_for(node), + name: u.children[0].to_s, + assignment: u.children[1], + asgn_code: u.children[1] ? region.code_for(u.children[1]) : nil, + presence: callable.location.range, + decl: get_decl(u) + ) + callable.parameters.push locals.last + end end end process_children @@ -29,16 +32,17 @@ def process private + # @param callable [Pin::Callable] # @return [void] - def forward + def forward(callable) loc = get_node_location(node) locals.push Solargraph::Pin::Parameter.new( location: loc, - closure: region.closure, + closure: callable, presence: region.closure.location.range, decl: get_decl(node) ) - region.closure.parameters.push locals.last + callable.parameters.push locals.last end # @param node [AST::Node] diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 5aaa753b4..fe056e60b 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -34,6 +34,7 @@ module Pin autoload :Singleton, 'solargraph/pin/singleton' autoload :KeywordParam, 'solargraph/pin/keyword_param' autoload :Search, 'solargraph/pin/search' + autoload :Callable, 'solargraph/pin/callable' ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil) end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 57239144f..d3a1089b5 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -2,7 +2,7 @@ module Solargraph module Pin - class Block < Closure + class Block < Callable # @return [Parser::AST::Node] attr_reader :receiver @@ -14,10 +14,9 @@ class Block < Closure # @param context [ComplexType, nil] # @param args [::Array] def initialize receiver: nil, args: [], context: nil, node: nil, **splat - super(**splat) + super(**splat, parameters: args) @receiver = receiver @context = context - @parameters = args @return_type = ComplexType.parse('::Proc') @node = node end @@ -32,16 +31,6 @@ def binder @rebind&.defined? ? @rebind : closure.binder end - # @return [::Array] - def parameters - @parameters ||= [] - end - - # @return [::Array] - def parameter_names - @parameter_names ||= parameters.map(&:name) - end - # @param yield_types [::Array] # @param parameters [::Array] # @@ -57,13 +46,6 @@ def destructure_yield_types(yield_types, parameters) parameters.map { ComplexType::UNDEFINED } end - # @todo the next step with parameters, arguments, destructuring, - # kwargs, etc logic is probably either creating a Parameters - # or Callable pin that encapsulates and shares the logic - # between methods, blocks and signatures. It could live in - # Signature if Method didn't also own potentially different - # set of parameters, generics and return types. - # @param api_map [ApiMap] # @return [::Array] def typify_parameters(api_map) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb new file mode 100644 index 000000000..33c05e176 --- /dev/null +++ b/lib/solargraph/pin/callable.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module Solargraph + module Pin + class Callable < Closure + # @return [self] + attr_reader :block + + attr_reader :parameters + + # @return [ComplexType, nil] + attr_reader :return_type + + # @param block [Signature, nil] + # @param return_type [ComplexType, nil] + # @param parameters [::Array] + def initialize block: nil, return_type: nil, parameters: [], **splat + super(**splat) + @block = block + @return_type = return_type + @parameters = parameters + end + + # @return [::Array] + def parameter_names + @parameter_names ||= parameters.map(&:name) + end + + # @param generics_to_resolve [Enumerable] + # @param arg_types [Array, nil] + # @param return_type_context [ComplexType, nil] + # @param yield_arg_types [Array, nil] + # @param yield_return_type_context [ComplexType, nil] + # @param context [ComplexType, nil] + # @param resolved_generic_values [Hash{String => ComplexType}] + # @return [self] + def resolve_generics_from_context(generics_to_resolve, + arg_types = nil, + return_type_context = nil, + yield_arg_types = nil, + yield_return_type_context = nil, + resolved_generic_values: {}) + callable = super(generics_to_resolve, return_type_context, resolved_generic_values: resolved_generic_values) + callable.parameters = callable.parameters.each_with_index.map do |param, i| + if arg_types.nil? + param.dup + else + param.resolve_generics_from_context(generics_to_resolve, + arg_types[i], + resolved_generic_values: resolved_generic_values) + end + end + callable.block = block.resolve_generics_from_context(generics_to_resolve, + yield_arg_types, + yield_return_type_context, + resolved_generic_values: resolved_generic_values) if callable.block? + callable + end + + # @param generics_to_resolve [Enumerable] + # @param arg_types [Array, nil] + # @param return_type_context [ComplexType, nil] + # @param yield_arg_types [Array, nil] + # @param yield_return_type_context [ComplexType, nil] + # @param context [ComplexType, nil] + # @param resolved_generic_values [Hash{String => ComplexType}] + # @return [self] + def resolve_generics_from_context_until_complete(generics_to_resolve, + arg_types = nil, + return_type_context = nil, + yield_arg_types = nil, + yield_return_type_context = nil, + resolved_generic_values: {}) + # See + # https://github.com/soutaro/steep/tree/master/lib/steep/type_inference + # and + # https://github.com/sorbet/sorbet/blob/master/infer/inference.cc + # for other implementations + + return self if generics_to_resolve.empty? + + last_resolved_generic_values = resolved_generic_values.dup + new_pin = resolve_generics_from_context(generics_to_resolve, + arg_types, + return_type_context, + yield_arg_types, + yield_return_type_context, + resolved_generic_values: resolved_generic_values) + if last_resolved_generic_values == resolved_generic_values + # erase anything unresolved + return new_pin.erase_generics(self.generics) + end + new_pin.resolve_generics_from_context_until_complete(generics_to_resolve, + arg_types, + return_type_context, + yield_arg_types, + yield_return_type_context, + resolved_generic_values: resolved_generic_values) + end + + # @return [Array] + # @yieldparam [ComplexType] + # @yieldreturn [ComplexType] + # @return [self] + def transform_types(&transform) + # @todo 'super' alone should work here I think, but doesn't typecheck at level typed + callable = super(&transform) + callable.block = block.transform_types(&transform) if block? + callable.parameters = parameters.map do |param| + param.transform_types(&transform) + end + callable + end + + # @param arguments [::Array] + # @param signature [Pin::Signature] + # @return [Boolean] + def arity_matches? arguments, with_block + argcount = arguments.length + parcount = mandatory_positional_param_count + parcount -= 1 if !parameters.empty? && parameters.last.block? + return false if block? && !with_block + return false if argcount < parcount && !(argcount == parcount - 1 && parameters.last.restarg?) + true + end + + def mandatory_positional_param_count + parameters.count(&:arg?) + end + + # @return [String] + def to_rbs + rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + '-> ' + return_type.to_rbs + end + + def block? + !!@block + end + + protected + + attr_writer :block + + attr_writer :parameters + end + end +end diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 0b8645355..939709de9 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -42,10 +42,15 @@ def generics end # @return [String] - def generics_as_rbs + def to_rbs + rbs_generics + return_type.to_rbs + end + + # @return [String] + def rbs_generics return '' if generics.empty? - generics.join(', ') + ' ' + '[' + generics.map { |gen| gen.to_s }.join(', ') + '] ' end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 753e1b3c3..bb8a1ba6f 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -4,12 +4,9 @@ module Solargraph module Pin # The base class for method and attribute pins. # - class Method < Closure + class Method < Callable include Solargraph::Parser::NodeMethods - # @return [::Array] - attr_reader :parameters - # @return [::Symbol] :public, :private, or :protected attr_reader :visibility @@ -18,24 +15,20 @@ class Method < Closure # @param visibility [::Symbol] :public, :protected, or :private # @param explicit [Boolean] - # @param parameters [::Array] # @param block [Pin::Signature, nil, ::Symbol] # @param node [Parser::AST::Node, nil] # @param attribute [Boolean] # @param signatures [::Array, nil] # @param anon_splat [Boolean] - # @param return_type [ComplexType, nil] - def initialize visibility: :public, explicit: true, parameters: [], block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, return_type: nil, **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 - @parameters = parameters @block = block @node = node @attribute = attribute @signatures = signatures @anon_splat = anon_splat - @return_type = return_type end def transform_types(&transform) @@ -44,9 +37,6 @@ def transform_types(&transform) m.signatures = m.signatures.map do |sig| sig.transform_types(&transform) end - m.parameters = m.parameters.map do |param| - param.transform_types(&transform) - end m.block = block&.transform_types(&transform) m.signature_help = nil m.documentation = nil @@ -71,17 +61,16 @@ def with_single_signature(signature) m end + def block? + !block.nil? + end + # @return [Pin::Signature, nil] def block return @block unless @block == :undefined @block = signatures.first.block end - # @return [::Array] - def parameter_names - @parameter_names ||= parameters.map(&:name) - end - def completion_item_kind attribute? ? Solargraph::LanguageServer::CompletionItemKinds::PROPERTY : Solargraph::LanguageServer::CompletionItemKinds::METHOD end @@ -123,9 +112,9 @@ def generate_signature(parameters, return_type) ) end yield_return_type = ComplexType.try_parse(*yieldreturn_tags.flat_map(&:types)) - block = Signature.new(generics, yield_parameters, yield_return_type) + block = Signature.new(generics: generics, parameters: yield_parameters, return_type: yield_return_type) end - Signature.new(generics, parameters, return_type, block) + Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block) end # @return [::Array] @@ -292,8 +281,8 @@ def overloads # tag's source is likely malformed. @overloads ||= docstring.tags(:overload).select(&:parameters).map do |tag| Pin::Signature.new( - generics, - tag.parameters.map do |src| + generics: generics, + parameters: tag.parameters.map do |src| name, decl = parse_overload_param(src.first) Pin::Parameter.new( location: location, @@ -305,7 +294,7 @@ def overloads return_type: param_type_from_name(tag, src.first) ) end, - ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types)) + return_type: ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types)) ) end @overloads @@ -319,8 +308,6 @@ def anon_splat? attr_writer :block - attr_writer :parameters - attr_writer :signatures attr_writer :signature_help @@ -475,6 +462,7 @@ def infer_from_iv api_map # @param name [String] # @return [::Array(String, ::Symbol)] def parse_overload_param(name) + # @todo this needs to handle mandatory vs not args, kwargs, blocks, etc if name.start_with?('**') [name[2..-1], :kwrestarg] elsif name.start_with?('*') @@ -496,6 +484,10 @@ def concat_example_tags .join("\n") .concat("```\n") end + + protected + + attr_writer :signatures end end end diff --git a/lib/solargraph/pin/namespace.rb b/lib/solargraph/pin/namespace.rb index f84a395cc..d4e66a354 100644 --- a/lib/solargraph/pin/namespace.rb +++ b/lib/solargraph/pin/namespace.rb @@ -41,7 +41,7 @@ def initialize type: :class, visibility: :public, gates: [''], **splat end def to_rbs - "#{@type.to_s} #{generics_as_rbs}#{return_type.to_rbs}" + "#{@type.to_s} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip end def desc diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ce7133899..d76309d0e 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -27,6 +27,10 @@ def kwrestarg? decl == :kwrestarg || (assignment && [:HASH, :hash].include?(assignment.type)) end + def arg? + decl == :arg + end + def restarg? decl == :restarg end diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index 725ac8387..da6f6a385 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -1,147 +1,17 @@ module Solargraph module Pin - class Signature < Base - # @return [::Array] - attr_reader :parameters - - # @return [ComplexType] - attr_reader :return_type - - # @return [self] - attr_reader :block - - # @param generics [Array] - # @param parameters [Array] - # @param return_type [ComplexType] - # @param block [Signature, nil] - def initialize generics, parameters, return_type, block = nil - @generics = generics - @parameters = parameters - @return_type = return_type - @block = block + class Signature < Callable + def initialize **splat + super(**splat) end def generics @generics ||= [].freeze end - # @return [String] - def to_rbs - rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + '-> ' + return_type.to_rbs - end - - # @return [String] - def rbs_generics - if generics.empty? - return '' - else - return '[' + generics.map { |gen| gen.to_s }.join(', ') + '] ' - end - end - - # @return [Array] - # @yieldparam [ComplexType] - # @yieldreturn [ComplexType] - # @return [self] - def transform_types(&transform) - # @todo 'super' alone should work here I think, but doesn't typecheck at level typed - signature = super(&transform) - signature.parameters = signature.parameters.map do |param| - param.transform_types(&transform) - end - signature.block = block.transform_types(&transform) if signature.block? - signature - end - - # Probe the concrete type for each of the generic type - # parameters used in this method, and return a new method pin if - # possible. - # - # @param generics_to_resolve [Enumerable] - # @param arg_types [Array, nil] - # @param return_type_context [ComplexType, nil] - # @param yield_arg_types [Array, nil] - # @param yield_return_type_context [ComplexType, nil] - # @param context [ComplexType, nil] - # @param resolved_generic_values [Hash{String => ComplexType}] - # @return [self] - def resolve_generics_from_context(generics_to_resolve, - arg_types = nil, - return_type_context = nil, - yield_arg_types = nil, - yield_return_type_context = nil, - resolved_generic_values: {}) - signature = super(generics_to_resolve, return_type_context, resolved_generic_values: resolved_generic_values) - signature.parameters = signature.parameters.each_with_index.map do |param, i| - if arg_types.nil? - param.dup - else - param.resolve_generics_from_context(generics_to_resolve, - arg_types[i], - resolved_generic_values: resolved_generic_values) - end - end - signature.block = block.resolve_generics_from_context(generics_to_resolve, - yield_arg_types, - yield_return_type_context, - resolved_generic_values: resolved_generic_values) if signature.block? - signature - end - - # @param generics_to_resolve [Enumerable] - # @param arg_types [Array, nil] - # @param return_type_context [ComplexType, nil] - # @param yield_arg_types [Array, nil] - # @param yield_return_type_context [ComplexType, nil] - # @param context [ComplexType, nil] - # @param resolved_generic_values [Hash{String => ComplexType}] - # @return [self] - def resolve_generics_from_context_until_complete(generics_to_resolve, - arg_types = nil, - return_type_context = nil, - yield_arg_types = nil, - yield_return_type_context = nil, - resolved_generic_values: {}) - # See - # https://github.com/soutaro/steep/tree/master/lib/steep/type_inference - # and - # https://github.com/sorbet/sorbet/blob/master/infer/inference.cc - # for other implementations - - return self if generics_to_resolve.empty? - - last_resolved_generic_values = resolved_generic_values.dup - new_pin = resolve_generics_from_context(generics_to_resolve, - arg_types, - return_type_context, - yield_arg_types, - yield_return_type_context, - resolved_generic_values: resolved_generic_values) - if last_resolved_generic_values == resolved_generic_values - # erase anything unresolved - return new_pin.erase_generics(self.generics) - end - new_pin.resolve_generics_from_context_until_complete(generics_to_resolve, - arg_types, - return_type_context, - yield_arg_types, - yield_return_type_context, - resolved_generic_values: resolved_generic_values) - end - def identity @identity ||= "signature#{object_id}" end - - def block? - !!@block - end - - protected - - attr_writer :block - - attr_writer :parameters end end end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 9737c2eba..60e487e5b 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -319,12 +319,12 @@ def method_def_to_pin decl, closure def method_def_to_sigs decl, pin decl.overloads.map do |overload| generics = overload.method_type.type_params.map(&:to_s) - parameters, return_type = parts_of_function(overload.method_type, pin) + signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) block = if overload.method_type.block - Pin::Signature.new(generics, *parts_of_function(overload.method_type.block, pin)) - end - return_type = ComplexType.try_parse(method_type_to_tag(overload.method_type)) - Pin::Signature.new(generics, parameters, return_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) + end + Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block) end end @@ -341,7 +341,7 @@ def location_decl_to_pin_location(location) # @param type [RBS::MethodType,RBS::Types::Block] # @param pin [Pin::Method] - # @return [Array, ComplexType>] + # @return [Array(Array, ComplexType)] def parts_of_function type, pin return [[Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin)], ComplexType.try_parse(method_type_to_tag(type))] if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 5a76ab094..dda2a69c8 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -68,7 +68,7 @@ def inferred_pins pins, api_map, context, locals sorted_overloads = overloads.sort { |ol| ol.block? ? -1 : 1 } new_signature_pin = nil sorted_overloads.each do |ol| - next unless arity_matches?(arguments, ol) + next unless ol.arity_matches?(arguments, with_block?) match = true atypes = [] @@ -192,19 +192,6 @@ def extra_return_type docstring, context nil end - # @param arguments [::Array] - # @param signature [Pin::Signature] - # @return [Boolean] - def arity_matches? arguments, signature - parameters = signature.parameters - argcount = arguments.length - parcount = parameters.length - parcount -= 1 if !parameters.empty? && parameters.last.block? - return false if signature.block? && !with_block? - return false if argcount < parcount && !(argcount == parcount - 1 && parameters.last.restarg?) - true - end - # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @return [::Array] diff --git a/spec/pin/namespace_spec.rb b/spec/pin/namespace_spec.rb index 2ff0e1073..a70e6c869 100644 --- a/spec/pin/namespace_spec.rb +++ b/spec/pin/namespace_spec.rb @@ -30,5 +30,6 @@ class Foo it 'uses @param tags as generic type parameters' do pin = Solargraph::Pin::Namespace.new(name: 'Foo', comments: '@generic GenericType') expect(pin.generics).to eq(['GenericType']) + expect(pin.to_rbs).to eq('class Foo[GenericType]') end end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 603ecedf1..e21125374 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -2266,4 +2266,41 @@ def blah clip = api_map.clip_at('test.rb', [10, 8]) expect(clip.infer.to_s).to eq('nil') end + + xit 'resolves overloads based on kwarg existence' do + source = Solargraph::Source.load_string(%( + class Blah + # @param *strings [Array] The type definitions to parse + # @return [Blah] + # @overload parse(*strings, partial:) + # @param *strings [Array] The type definitions to parse + # @param partial [Boolean] True if the string is part of a another type + # @return [Array] + def self.parse *strings, partial: false; end + + def foo + x = Blah.parse('blah') + x + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [12, 10]) + expect(clip.infer.to_s).to eq('Blah') + end + + it 'handles resolving String#each_line overloads' do + source = Solargraph::Source.load_string(%( + def foo + 'abc\ndef'.each_line do |line| + line + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [3, 10]) + expect(clip.infer.to_s).to eq('String') + end end From c008197d873157a845c7dd6c584eb7568d5f2d6c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 19 Apr 2025 10:44:55 -0400 Subject: [PATCH 009/116] Adjust local variable presence to start after assignment, not before (#864) * Adjust local variable presence to start after assignment, not before * Add regression test around assignment in return position * Fix assignment visibility code, which relied on bad asgn semantics --- .../parser/parser_gem/node_chainer.rb | 20 +++--- .../parser_gem/node_processors/lvasgn_node.rb | 4 +- spec/parser/node_chainer_spec.rb | 5 +- spec/source_map/clip_spec.rb | 62 +++++++++++++++++-- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index bb5d7655c..b8d69f7a2 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -89,15 +89,21 @@ def generate_links n elsif n.type == :const const = unpack_name(n) result.push Chain::Constant.new(const) - elsif [:lvar, :lvasgn].include?(n.type) + elsif [:lvasgn, :ivasgn, :gvasgn, :cvasgn].include?(n.type) + result.concat generate_links(n.children[1]) + elsif n.type == :lvar result.push Chain::Call.new(n.children[0].to_s) - elsif [:ivar, :ivasgn].include?(n.type) - result.push Chain::InstanceVariable.new(n.children[0].to_s) - elsif [:cvar, :cvasgn].include?(n.type) - result.push Chain::ClassVariable.new(n.children[0].to_s) - elsif [:gvar, :gvasgn].include?(n.type) - result.push Chain::GlobalVariable.new(n.children[0].to_s) + elsif n.type == :ivar + result.push Chain::InstanceVariable.new(n.children[0].to_s) + elsif n.type == :cvar + result.push Chain::ClassVariable.new(n.children[0].to_s) + elsif n.type == :gvar + result.push Chain::GlobalVariable.new(n.children[0].to_s) elsif n.type == :or_asgn + # @todo: Need a new Link class here that evaluates the + # existing variable type with the RHS, and generates a + # union type of the LHS alone if never nil, or minus nil + + # RHS if it is nilable. result.concat generate_links n.children[1] elsif [:class, :module, :def, :defs].include?(n.type) # @todo Undefined or what? diff --git a/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb index efff73645..47e79d66d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb @@ -8,8 +8,8 @@ class LvasgnNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - here = get_node_start_position(node) - presence = Range.new(here, region.closure.location.range.ending) + # variable not visible until next statement + presence = Range.new(get_node_end_position(node), region.closure.location.range.ending) loc = get_node_location(node) locals.push Solargraph::Pin::LocalVariable.new( location: loc, diff --git a/spec/parser/node_chainer_spec.rb b/spec/parser/node_chainer_spec.rb index e92431aae..fee801e7c 100644 --- a/spec/parser/node_chainer_spec.rb +++ b/spec/parser/node_chainer_spec.rb @@ -111,10 +111,9 @@ class Foo foo = [1, 2] )) chain = Solargraph::Parser.chain(source.node) - expect(chain.links.map(&:word)).to eq(['foo']) + expect(chain.links.map(&:word)).to eq(['<::Array>']) foo_link = chain.links.first - expect(foo_link.class).to eq(Solargraph::Source::Chain::Call) - expect(foo_link.arguments).to eq([]) + expect(foo_link.class).to eq(Solargraph::Source::Chain::Array) end it 'tracks complex lhs' do diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index e21125374..efadae775 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -361,10 +361,11 @@ def bar # @return [String, Array] def foo; end var = foo + var ), 'test.rb') map = Solargraph::ApiMap.new map.map source - clip = map.clip_at('test.rb', Solargraph::Position.new(3, 7)) + clip = map.clip_at('test.rb', Solargraph::Position.new(4, 6)) type = clip.infer expect(type.to_s).to eq('String, Array') end @@ -731,10 +732,11 @@ def self.new end end value = Value.new + value ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source - clip = api_map.clip_at('test.rb', [6, 11]) + clip = api_map.clip_at('test.rb', [7, 11]) expect(clip.infer.tag).to eq('Class') end @@ -1252,6 +1254,7 @@ class Foo class Mod def meth arr = [] + arr 1.times do arr end @@ -1261,11 +1264,11 @@ def meth ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source - clip = api_map.clip_at('test.rb', [3, 11]) + clip = api_map.clip_at('test.rb', [4, 11]) expect(clip.infer.tag).to eq('Array') - clip = api_map.clip_at('test.rb', [5, 12]) + clip = api_map.clip_at('test.rb', [6, 12]) expect(clip.infer.tag).to eq('Array') - clip = api_map.clip_at('test.rb', [7, 10]) + clip = api_map.clip_at('test.rb', [8, 10]) expect(clip.infer.tag).to eq('Array') end @@ -2267,6 +2270,55 @@ def blah expect(clip.infer.to_s).to eq('nil') end + it 'can infer assignments-in-return-position from complex expressions' do + source = Solargraph::Source.load_string(%( + class A + def foo + blah = ['foo'].map { 456 } + end + + def bar + nah ||= ['foo'].map { 456 } + end + + def foo1 + @blah = ['foo'].map { 456 } + end + + def bar1 + @nah2 ||= ['foo'].map { 456 } + end + + def baz + a = foo + a + b = bar + b + a1 = foo1 + a1 + b2 = bar1 + b2 + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [20, 10]) + expect(clip.infer.to_s).to eq('Array') + + # @todo pending https://github.com/castwide/solargraph/pull/888 + # clip = api_map.clip_at('test.rb', [22, 10]) + # expect(clip.infer.to_s).to eq('Array') + + clip = api_map.clip_at('test.rb', [24, 10]) + expect(clip.infer.to_s).to eq('Array') + + # @todo pending https://github.com/castwide/solargraph/pull/888 + # clip = api_map.clip_at('test.rb', [26, 10]) + # expect(clip.infer.to_s).to eq('Array') + end + xit 'resolves overloads based on kwarg existence' do source = Solargraph::Source.load_string(%( class Blah From 9aa5e368b456487e7b6ca1830b5fc6e386c4c6b7 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Sat, 19 Apr 2025 10:46:11 -0400 Subject: [PATCH 010/116] Resolve params from ref tags (#872) * Resolve params from ref tags * Resolve ref tags with namespaces --- lib/solargraph/pin/method.rb | 24 ++++++++++++++++++ lib/solargraph/pin/parameter.rb | 10 ++------ spec/pin/method_spec.rb | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index bb8a1ba6f..e2fed6626 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -272,6 +272,7 @@ def probe api_map def try_merge! pin return false unless super @node = pin.node + @resolved_ref_tag = false true end @@ -304,6 +305,29 @@ def anon_splat? @anon_splat end + # @param [ApiMap] + # @return [self] + def resolve_ref_tag api_map + return self if @resolved_ref_tag + + @resolved_ref_tag = true + return self unless docstring.ref_tags.any? + docstring.ref_tags.each do |tag| + ref = if tag.owner.to_s.start_with?(/[#\.]/) + api_map.get_methods(namespace) + .select { |pin| pin.path.end_with?(tag.owner.to_s) } + .first + else + # @todo Resolve relative namespaces + api_map.get_path_pins(tag.owner.to_s).first + end + next unless ref + + docstring.add_tag(*ref.docstring.tags(:param)) + end + self + end + protected attr_writer :block diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index d76309d0e..878f1deea 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -130,17 +130,11 @@ def try_merge! pin # @return [YARD::Tags::Tag, nil] def param_tag - found = nil params = closure.docstring.tags(:param) params.each do |p| - next unless p.name == name - found = p - break + return p if p.name == name end - if found.nil? and !index.nil? - found = params[index] if params[index] && (params[index].name.nil? || params[index].name.empty?) - end - found + params[index] if index && params[index] && (params[index].name.nil? || params[index].name.empty?) end # @param api_map [ApiMap] diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index bce1dfd11..4e367c521 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -461,6 +461,50 @@ def bar?; end expect(pin.documentation).to include('# Call foo') end + it 'resolves ref tags' do + source = Solargraph::Source.load_string(%( + class Example + # @param param1 [String] + # @param param2 [Integer] + def foo param1, param2 + end + + # @param (see #foo) + def bar param1, param2 + end + end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('Example#bar').first + pin.resolve_ref_tag(api_map) + expect(pin.docstring.tags(:param).map(&:name)).to eq(['param1', 'param2']) + expect(pin.docstring.tags(:param).map(&:type)).to eq(['String', 'Integer']) + end + + it 'resolves ref tags with namespaces' do + source = Solargraph::Source.load_string(%( + class Example1 + # @param param1 [String] + # @param param2 [Integer] + def foo param1, param2 + end + end + + class Example2 + # @param (see Example1#foo) + def bar param1, param2 + end + end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('Example2#bar').first + pin.resolve_ref_tag(api_map) + expect(pin.docstring.tags(:param).map(&:name)).to eq(['param1', 'param2']) + expect(pin.docstring.tags(:param).map(&:type)).to eq(['String', 'Integer']) + end + context 'as attribute' do it "is a kind of attribute/property" do source = Solargraph::Source.load_string(%( From a94221a25b0fa15b11427ec801c9ee69aa49b8b6 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Sat, 19 Apr 2025 12:27:37 -0400 Subject: [PATCH 011/116] Remove Library#folding_ranges --- lib/solargraph/library.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 17acd5baf..6a02be3fc 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -441,17 +441,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 From cb29ac59561945f19c0155f8bf6ee5aa9c569d21 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 20 Apr 2025 10:31:09 -0400 Subject: [PATCH 012/116] Disable a couple of particularly noisy logs by default --- lib/solargraph/complex_type.rb | 2 +- lib/solargraph/complex_type/unique_type.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index b8348d2e6..1dc402c76 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -175,7 +175,7 @@ def force_rooted def resolve_generics definitions, context_type result = @items.map { |i| i.resolve_generics(definitions, context_type) } out = ComplexType.try_parse(*result.map(&:tag)) - logger.debug { "ComplexType#resolve_generics(self=#{rooted_tags}, definitions=#{definitions}, context_type=#{context_type.rooted_tags} => #{out.rooted_tags}" } + # logger.debug { "ComplexType#resolve_generics(self=#{rooted_tags}, definitions=#{definitions}, context_type=#{context_type.rooted_tags} => #{out.rooted_tags}" } out end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index f1c03054e..8757f08c4 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -216,7 +216,7 @@ 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}" } + # logger.debug { "UniqueType#resolve_generics(self=#{self.rooted_tag}, definitions=#{definitions}, context_type=#{context_type.rooted_tags}) => #{out}" } out end From 67dab545260166f904d643f033b92ff40ab2f8db Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 20 Apr 2025 11:17:45 -0400 Subject: [PATCH 013/116] Fix numeric vs symbol logic --- lib/solargraph/logging.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index e3173e78c..9640be39a 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -27,15 +27,16 @@ module Logging # show existing debug messages by setting to :debug). @return # [Symbol] def log_level - @@logger.level + :warn end # @return [Logger] def logger - @logger ||= if log_level == @@logger.level + @logger ||= if LOG_LEVELS[log_level.to_s] == @@logger.level @@logger else - logger = Logger.new(STDERR, log_level) + new_log_level = LOG_LEVELS[log_level.to_s] + logger = Logger.new(STDERR, level: new_log_level) logger.formatter = @@logger.formatter logger end From eb49ddbf1279a9be25c98c062491c5d78d6bfa1c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 21 Apr 2025 07:20:07 -0400 Subject: [PATCH 014/116] Avoid marshaling issues --- lib/solargraph/logging.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index 9640be39a..7c7531aea 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -32,14 +32,14 @@ def log_level # @return [Logger] def logger - @logger ||= if LOG_LEVELS[log_level.to_s] == @@logger.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 + 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 From 4eab3f2a360f74a6fee10f3ec99bf218738c2fb0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 24 Apr 2025 10:22:22 -0400 Subject: [PATCH 015/116] Various debugging-related log statements --- lib/solargraph/pin/base_variable.rb | 1 + lib/solargraph/pin/method.rb | 10 ++++++++-- lib/solargraph/pin/parameter.rb | 18 +++++++++++++++--- lib/solargraph/source/chain.rb | 6 +++--- lib/solargraph/source/chain/call.rb | 19 ++++++++++--------- lib/solargraph/source_map.rb | 6 +++++- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 4c42351aa..dd59398f2 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -61,6 +61,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 diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 6d0b106ac..3423ee937 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -184,7 +184,7 @@ def typify api_map logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}, return_type=#{return_type.rooted_tags}) - starting" } decl = super unless decl.undefined? - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl}" } + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl} - no decl" } return decl end type = see_reference(api_map) || typify_from_super(api_map) @@ -194,7 +194,13 @@ def typify api_map logger.debug { "Method#typify(self=#{self}) => #{qualified}" } return qualified end - name.end_with?('?') ? ComplexType::BOOLEAN : ComplexType::UNDEFINED + 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 def documentation diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 34b33812d..393bbd2b6 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -111,9 +111,21 @@ def index # @param api_map [ApiMap] def typify api_map - logger.debug { "Parameter#typify(closure=#{closure.inspect}) - starting" } - 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(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) - starting" } + unless return_type.undefined? + out = return_type.qualify(api_map, closure.context.namespace) + logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from declaration" } + return out + end + if closure.is_a?(Pin::Block) + out = typify_block_param(api_map) + logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from block parameter" } + out + else + out = typify_method_param(api_map) + logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from method parameter" } + out + end end def documentation diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index c267b9462..9495f5f10 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -78,7 +78,7 @@ def base # # @return [::Array] def define api_map, name_pin, locals - logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) - starting" } + logger.debug { "Chain#define(name_pin=#{name_pin.desc}, links=#{links.map(&:desc)}, locals=#{locals}) - starting" } return [] if undefined? working_pin = name_pin links[0..-2].each do |link| @@ -93,7 +93,7 @@ def define api_map, name_pin, locals end links.last.last_context = name_pin out = links.last.resolve(api_map, working_pin, locals) - logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.inspect}, locals=#{locals}) => #{out}" } + logger.debug { "Chain#define(name_pin=#{name_pin.desc}, links=#{links.map(&:desc)}, locals=#{locals}) => #{out}" } out end @@ -127,7 +127,7 @@ def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) - logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)} => #{out}" } + logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)}, locals=#{locals.map(&:desc)}) => #{out}" } out end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 4f706f847..9de17606d 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -31,7 +31,7 @@ def with_block? # @param name_pin [Pin::Base] # @param locals [::Array] def resolve api_map, name_pin, locals - logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, name_pin=#{name_pin}) - starting" } + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, 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? @@ -39,6 +39,7 @@ def resolve api_map, name_pin, locals else [] end + 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.context, locals) logger.debug { "Call#resolve(word=#{word}, name_pin=#{name_pin}) - found=#{found} => #{out}" } @@ -49,11 +50,11 @@ def resolve api_map, name_pin, locals api_map.get_method_stack(context.namespace == '' ? '' : context.tag, word, scope: context.scope) end if pins.empty? - logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } + logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder.rooted_tags}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } return [] end out = inferred_pins(pins, api_map, name_pin.context, locals) - logger.debug { "Call#resolve(name_pin.binder=#{name_pin.binder}, word=#{word}, name_pin=#{name_pin}) - pins=#{pins.map(&:desc)} => #{out}" } + 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 @@ -86,7 +87,7 @@ def inferred_pins pins, api_map, context, locals atypes = [] sorted_overloads.each do |ol| 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}" } + 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 @@ -96,9 +97,9 @@ def inferred_pins pins, api_map, context, locals if param.nil? match = ol.parameters.any?(&:restarg?) if match - logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - accepting rest via restarg - #{ol}" } + 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(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - more args than parameters found - #{arg} not matched - #{ol} not matched" } + 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 @@ -108,7 +109,7 @@ def inferred_pins pins, api_map, context, locals # @todo Weak type comparison # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) unless param.return_type.undefined? || atype.name == param.return_type.name || api_map.super_and_sub?(param.return_type.name, atype.name) || param.return_type.generic? - logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - rejecting signature #{ol}" } + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - rejecting signature #{ol}" } match = false break end @@ -126,9 +127,9 @@ def inferred_pins pins, api_map, context, locals break if type.defined? end if new_signature_pin.nil? - logger.debug { "Call#inferred_pins(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - found no matching signatures for #{p}" } + 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(word=#{word}, name_pin=#{name_pin}, name_pin.binder=#{name_pin.binder}) - accepting signature #{new_signature_pin}" } + 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? diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 82536879b..83e9acbc1 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -143,7 +143,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 @@ -202,5 +204,7 @@ def _locate_pin line, character, *klasses # Assuming the root pin is always valid found || pins.first end + + include Logging end end From d4b7644bded338c40991517f3cccc77142dc5ea7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 24 Apr 2025 10:26:16 -0400 Subject: [PATCH 016/116] Add missing include statement --- lib/solargraph/pin/base_variable.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index dd59398f2..96ef2c29e 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -105,6 +105,8 @@ def desc "#{to_rbs} = #{assignment&.type.inspect}" end + include Logging + private # @return [ComplexType] From 5379efb5e5c0160f86db1adbafb8cda6adc477ab Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 26 Apr 2025 07:43:27 -0400 Subject: [PATCH 017/116] Various debugging-related log statements --- lib/solargraph/complex_type.rb | 2 +- lib/solargraph/pin/block.rb | 2 ++ lib/solargraph/pin/method.rb | 6 +++--- lib/solargraph/source/chain/call.rb | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index d38053f9a..54ede2747 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -43,7 +43,7 @@ def qualify api_map, context = '' t.qualify api_map, context end out = ComplexType.new(types).reduce_object - logger.debug { "ComplexType#qualify(self=#{self}, context=#{context.inspect}) => #{out.rooted_tags}" } + logger.debug { "ComplexType#qualify(self=#{self.rooted_tags}, context=#{context.inspect}) => #{out.rooted_tags}" } out end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 3fe439f48..8e0cfd630 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -98,9 +98,11 @@ 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? diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 2f465e41a..906bf9659 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -185,17 +185,17 @@ def path end def typify api_map - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}, return_type=#{return_type.rooted_tags}) - starting" } + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" } decl = super unless decl.undefined? - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl} - no decl" } + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags} - decl found" } return decl end type = see_reference(api_map) || typify_from_super(api_map) logger.debug { "Method#typify(self=#{self}) - type=#{type}" } unless type.nil? qualified = type.qualify(api_map, namespace) - logger.debug { "Method#typify(self=#{self}) => #{qualified}" } + logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags}" } return qualified end if name.end_with?('?') diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 6eb0c1e8e..d21e91947 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -53,7 +53,7 @@ def resolve api_map, name_pin, locals api_map.get_method_stack(method_context, word, scope: context.scope) end 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}, name_pin.binder=#{name_pin.binder}) => [] - found no pins for #{word} in #{name_pin.binder}" } + 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) @@ -128,6 +128,7 @@ def inferred_pins pins, api_map, name_pin, locals end end 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 type = with_params(new_return_type.self_to_type(name_pin.context), name_pin.context).qualify(api_map, name_pin.context.namespace) if new_return_type.defined? type ||= ComplexType::UNDEFINED From 55a2e26c963e6ed09a4ae5159f33de6405253aa4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 28 Apr 2025 08:00:53 -0400 Subject: [PATCH 018/116] Various debugging-related log statements --- lib/solargraph/complex_type/unique_type.rb | 4 +++- lib/solargraph/pin/block.rb | 7 ++++++- lib/solargraph/pin/method.rb | 6 +++--- lib/solargraph/pin/parameter.rb | 8 ++++---- lib/solargraph/source/chain.rb | 2 +- lib/solargraph/source/chain/array.rb | 4 +++- lib/solargraph/source/chain/call.rb | 6 ++++-- lib/solargraph/source/chain/link.rb | 2 ++ lib/solargraph/type_checker.rb | 3 +++ 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 494710f7f..c4cf64ce0 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -315,7 +315,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) @@ -326,6 +326,8 @@ def qualify api_map, context = '' end t.recreate(new_name: fqns, make_rooted: true) end + logger.debug { "UniqueType#qualify(self=#{rooted_tags.inspect}) => #{out.rooted_tags.inspect}" } + out end def selfy? diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 8e0cfd630..f60df2709 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -106,11 +106,16 @@ def maybe_rebind api_map types = receiver_pin.docstring.tag(:yieldreceiver)&.types return ComplexType::UNDEFINED unless types&.any? + 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/method.rb b/lib/solargraph/pin/method.rb index 906bf9659..ce45b5571 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -188,14 +188,14 @@ def typify api_map logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" } decl = super unless decl.undefined? - logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags} - decl found" } + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags.inspect} - decl found" } return decl end type = see_reference(api_map) || typify_from_super(api_map) - logger.debug { "Method#typify(self=#{self}) - type=#{type}" } + logger.debug { "Method#typify(self=#{self}) - type=#{type.rooted_tags.inspect}" } unless type.nil? qualified = type.qualify(api_map, namespace) - logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags}" } + logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } return qualified end if name.end_with?('?') diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index be0fd43b6..872c98d0c 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -111,19 +111,19 @@ def index # @param api_map [ApiMap] def typify api_map - logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) - starting" } + 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(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from declaration" } + logger.debug { "Parameter#typify(self=#{self.desc}, return_type=#{return_type.rooted_tags}, ) => #{out} from declaration" } return out end if closure.is_a?(Pin::Block) out = typify_block_param(api_map) - logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from block parameter" } + logger.debug { "Parameter#typify(self=#{self.desc}) => #{out} from block parameter" } out else out = typify_method_param(api_map) - logger.debug { "Parameter#typify(name=#{name}, return_type=#{return_type.rooted_tags}, closure.context.namespace=#{closure.context.namespace.inspect}, closure.context=#{closure.context}, closure=#{closure.inspect}) => #{out} from method parameter" } + logger.debug { "Parameter#typify(self=#{self.desc}) => #{out} from method parameter" } out end end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 268d6fd7d..3c9b9da96 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -139,7 +139,7 @@ def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) type = infer_first_defined(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) - logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)}, locals=#{locals.map(&:desc)}) => #{out}" } + logger.debug { "Chain#infer_uncached(links=#{self.links.map(&:desc)}, locals=#{locals.map(&:desc)}, name_pin=#{name_pin}, name_pin.closure=#{name_pin.closure.inspect}, name_pin.binder=#{name_pin.binder}) => #{out.rooted_tags.inspect}" } out end diff --git a/lib/solargraph/source/chain/array.rb b/lib/solargraph/source/chain/array.rb index 63706ad03..2fca70690 100644 --- a/lib/solargraph/source/chain/array.rb +++ b/lib/solargraph/source/chain/array.rb @@ -25,7 +25,9 @@ def resolve api_map, name_pin, locals else ComplexType::UniqueType.new('Array', rooted: true) end - [Pin::ProxyType.anonymous(type)] + out = [Pin::ProxyType.anonymous(type)] + 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 7299b314f..05d15080a 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -52,6 +52,7 @@ def resolve api_map, name_pin, locals method_context = context.namespace == '' ? '' : context.tag api_map.get_method_stack(method_context, word, scope: context.scope) end + 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 [] @@ -120,6 +121,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) @@ -151,7 +153,7 @@ def inferred_pins pins, api_map, name_pin, locals end p end - logger.debug { "Call#inferred_pins(pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) - result=#{result}" } + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) - result=#{result}" } out = result.map do |pin| if pin.path == 'Class#new' && name_pin.context.tag != 'Class' reduced_context = name_pin.context.reduce_class_type @@ -162,7 +164,7 @@ def inferred_pins pins, api_map, name_pin, locals selfy == pin.return_type ? pin : pin.proxy(selfy) end end - logger.debug { "Call#inferred_pins(pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) => #{out}" } + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, pins=#{pins.map(&:desc)}, name_pin=#{name_pin}) => #{out}" } out end diff --git a/lib/solargraph/source/chain/link.rb b/lib/solargraph/source/chain/link.rb index ec33f8045..9b67c204e 100644 --- a/lib/solargraph/source/chain/link.rb +++ b/lib/solargraph/source/chain/link.rb @@ -70,6 +70,8 @@ def inspect "#<#{self.class} - `#{self.desc}`>" end + include Logging + protected # Mark whether this link is the head of a chain diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 081cc556e..b92397ef3 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -266,6 +266,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 @@ -668,5 +669,7 @@ def without_ignored problems node && source_map.source.comments_for(node)&.include?('@sg-ignore') end end + + include Logging end end From 5117e3cd1daa3f6ec5c770ead4660ebdbce611be Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 28 Apr 2025 16:26:17 -0400 Subject: [PATCH 019/116] Fix merge issues --- lib/solargraph/source/chain/call.rb | 2 +- spec/source_map/clip_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 200428b87..d794f14af 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -114,7 +114,7 @@ def inferred_pins pins, api_map, name_pin, locals ptype = param.typify api_map # @todo Weak type comparison # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) - unless param.return_type.undefined? || atype.name == param.return_type.name || api_map.super_and_sub?(param.return_type.name, atype.name) || param.return_type.generic? + unless ptype.undefined? || atype.name == ptype.name || ptype.any? { |current_ptype| api_map.super_and_sub?(current_ptype.name, atype.name) } || ptype.generic? || param.restarg? logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - rejecting signature #{ol}" } match = false break diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index ae6940eba..9f90a9877 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -2390,7 +2390,7 @@ def inferred_pins pins clip = api_map.clip_at('test.rb', [13, 14]) expect(clip.infer.tags).to eq('String') end - + it 'handles block method yield scenarios' do source = Solargraph::Source.load_string(%( # @yieldreturn [Integer] From beacbc18b8ef337f8313c87279aefc34a102797a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 29 Apr 2025 10:47:12 -0400 Subject: [PATCH 020/116] Fix merge issue --- lib/solargraph/rbs_map/conversions.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 7ee647abd..504a1c84a 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -211,7 +211,6 @@ def module_decl_to_pin decl # # @return [Solargraph::Pin::Constant] def create_constant(name, tag, comments, decl, base = nil) - comments = decl.comment&.string parts = name.split('::') if parts.length > 1 name = parts.last From d2b017cb9e9c431d31f39be2e76f2a5992425216 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 29 Apr 2025 10:52:37 -0400 Subject: [PATCH 021/116] Add missing bits --- lib/solargraph/complex_type.rb | 4 ++++ lib/solargraph/pin/method.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 54ede2747..2218aee4d 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -157,6 +157,10 @@ def to_s map(&:tag).join(', ') end + def desc + rooted_tags + end + def rooted_tags map(&:rooted_tag).join(', ') end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index ce45b5571..51b3cdabd 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -192,7 +192,7 @@ def typify api_map return decl end type = see_reference(api_map) || typify_from_super(api_map) - logger.debug { "Method#typify(self=#{self}) - type=#{type.rooted_tags.inspect}" } + logger.debug { "Method#typify(self=#{self}) - type=#{type&.rooted_tags.inspect}" } unless type.nil? qualified = type.qualify(api_map, namespace) logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } From c2fb9f71e7ac3d5e307b21df48aba26d692867c0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 May 2025 09:51:42 -0400 Subject: [PATCH 022/116] Improve Parameter logs --- lib/solargraph/pin/parameter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index f4cedef66..5c26b1a47 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -114,16 +114,16 @@ def typify 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} from declaration" } + 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} from block parameter" } + 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} from method parameter" } + logger.debug { "Parameter#typify(self=#{self.desc}) => #{out.rooted_tags} from method parameter" } out end end From 153f78e7a77a6aec91affe2b01048779c961f8ec Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 May 2025 20:35:51 -0400 Subject: [PATCH 023/116] More debug logging --- lib/solargraph/api_map.rb | 29 +++++++++++++++---- lib/solargraph/api_map/store.rb | 10 +++++-- lib/solargraph/complex_type/unique_type.rb | 2 +- lib/solargraph/parser/node_processor/base.rb | 2 ++ .../parser/parser_gem/node_chainer.rb | 7 ++++- .../parser_gem/node_processors/cvasgn_node.rb | 6 +++- .../node_processors/namespace_node.rb | 4 ++- lib/solargraph/source/chain/call.rb | 4 +-- lib/solargraph/source/chain/class_variable.rb | 4 ++- lib/solargraph/source/chain/constant.rb | 3 ++ lib/solargraph/source_map/clip.rb | 13 ++++++++- 11 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index aad4f6b12..9f8402a47 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -153,8 +153,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. @@ -228,6 +232,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) @@ -236,11 +241,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 @@ -293,6 +300,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) @@ -300,6 +308,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 @@ -327,7 +336,9 @@ def get_instance_variable_pins(namespace, scope = :instance) # @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] @@ -689,14 +700,20 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false # @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 @@ -704,6 +721,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 @@ -735,6 +753,7 @@ def qualify_superclass fqsub # @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 cd6bfacac..9a3c628a0 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -104,7 +104,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] @@ -177,9 +179,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 # @return [Hash{::Array(String, String) => ::Array}] diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4acd4d342..bd57fb18e 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -330,7 +330,7 @@ def qualify api_map, context = '' end t.recreate(new_name: fqns, make_rooted: true) end - logger.debug { "UniqueType#qualify(self=#{rooted_tags.inspect}) => #{out.rooted_tags.inspect}" } + logger.debug { "UniqueType#qualify(self=#{rooted_tags.inspect}, context=#{context}) => #{out.rooted_tags.inspect}" } out 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 b8d69f7a2..10ae94645 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 31b956486..a465838ee 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb @@ -7,15 +7,19 @@ 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, comments: comments_for(node), assignment: node.children[1] ) + 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 0255c933a..3954f75ba 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -13,15 +13,17 @@ def process sc = unpack_name(node.children[1]) end 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 ) + logger.debug { "NamespaceNode#process: Created namespace pin: #{nspin} in closure #{region.closure} and namespace=#{nspin.namespace} and name=#{name}" } pins.push nspin unless sc.nil? pins.push Pin::Reference::Superclass.new( diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index ca663b7ce..bcb448fad 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -33,7 +33,7 @@ def with_block? # @param name_pin [Pin::Closure] name_pin.binder should give us 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}, word=#{word}, arguments=#{arguments.map(&:desc)}, name_pin=#{name_pin}) - starting" } + 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? @@ -115,7 +115,7 @@ def inferred_pins pins, api_map, name_pin, locals # @todo Weak type comparison # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) unless ptype.undefined? || atype.name == ptype.name || ptype.any? { |current_ptype| api_map.super_and_sub?(current_ptype.name, atype.name) } || ptype.generic? || param.restarg? - logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - rejecting signature #{ol}" } + logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, name_pin.context=#{name_pin.context}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - rejecting signature #{ol}" } match = false break end 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 659092c70..28024e0a3 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_map/clip.rb b/lib/solargraph/source_map/clip.rb index fc9cc7a53..140901fdf 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -30,12 +30,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 @@ -88,6 +95,8 @@ def translate phrase chain.define(api_map, block, locals) end + include Logging + private # @return [ApiMap] @@ -174,6 +183,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? @@ -189,6 +199,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) From 2875d687f596f22eaac4e05326b8de2309794585 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 May 2025 10:11:36 -0400 Subject: [PATCH 024/116] Re-enable support for .gem_rbs_collection directories --- .github/workflows/plugins.yml | 2 ++ .github/workflows/typecheck.yml | 2 ++ .gitignore | 2 ++ README.md | 12 ++++++++++-- lib/solargraph/rbs_map.rb | 4 +--- lib/solargraph/workspace.rb | 4 +++- rbs_collection.yaml | 19 +++++++++++++++++++ 7 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 rbs_collection.yaml diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 0f2fe01c1..b0f22ec3e 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -34,6 +34,8 @@ jobs: bundle exec solargraph config yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml + - name: Install gem types + run: bundle exec rbs collection install - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level typed - name: Ensure specs still run diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 5b1b5e151..ed3472308 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -30,5 +30,7 @@ jobs: bundler-cache: false - name: Install gems run: bundle install + - name: Install gem types + run: bundle exec rbs collection install - name: Typecheck self run: bundle exec solargraph typecheck --level typed diff --git a/.gitignore b/.gitignore index e2ba3cb38..fc09c2fea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/.gem_rbs_collection +/rbs_collection.lock.yaml /Gemfile.lock .Gemfile .idea diff --git a/README.md b/README.md index 30edafcf0..fd65e0804 100755 --- a/README.md +++ b/README.md @@ -63,10 +63,18 @@ The RSpec framework is supported via [solargraph-rspec](https://github.com/lekem **Note: Before version 0.53.0, it was recommended to run `yard gems` periodically or automate it with `yard config` to ensure that Solargraph had access to gem documentation. These steps are no longer necessary. Solargraph maintains its own gem documentation cache independent of the yardocs in your gem installations.** -Solargraph automatically generates code maps from installed gems. You can also manage your cached gem documentation with the `solargraph gems` command. - When editing code, a `require` call that references a gem will pull the documentation into the code maps and include the gem's API in code completion and intellisense. +Solargraph automatically generates code maps from installed gems. You can also manage your cached gem documentation with the `solargraph gems` command. + +To combine this YARD and Rdoc information with RBS, use [gem\_rbs\_collection](https://github.com/ruby/gem_rbs_collection) +to install RBS types for Rails: + +```sh +bundle exec rbs collection init +bundle exec rbs collection install +``` + If your project automatically requires bundled gems (e.g., `require 'bundler/require'`), Solargraph will add all of the Gemfile's default dependencies to the map. ### Type Checking diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 52502facc..2edfd4fb3 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -52,9 +52,7 @@ def resolved? def repository @repository ||= RBS::Repository.new(no_stdlib: false).tap do |repo| - # @todo Temporarily ignoring external directories due to issues with - # incomplete/broken gem_rbs_collection installations - # @directories.each { |dir| repo.add(Pathname.new(dir)) } + @directories.each { |dir| repo.add(Pathname.new(dir)) } end end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index e47ca0eb1..7d8fd73e3 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -233,7 +233,9 @@ def read_rbs_collection_path yaml_file = File.join(directory, 'rbs_collection.yaml') return unless File.file?(yaml_file) - YAML.load_file(yaml_file)&.fetch('path') + path = YAML.load_file(yaml_file)&.fetch('path') + # make fully qualified + File.expand_path(path, directory) end end end diff --git a/rbs_collection.yaml b/rbs_collection.yaml new file mode 100644 index 000000000..66e30ecfe --- /dev/null +++ b/rbs_collection.yaml @@ -0,0 +1,19 @@ +# Download sources +sources: + - type: git + name: ruby/gem_rbs_collection + remote: https://github.com/ruby/gem_rbs_collection.git + revision: main + repo_dir: gems + +# You can specify local directories as sources also. +# - type: local +# path: path/to/your/local/repository + +# A directory to install the downloaded RBSs +path: .gem_rbs_collection + +# gems: +# # If you want to avoid installing rbs files for gems, you can specify them here. +# - name: GEM_NAME +# ignore: true From 961d2e0e5d274e5b1c00c63868e1897ffeabc5f5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 May 2025 15:39:58 -0400 Subject: [PATCH 025/116] Try harder fetching RBS from gem --- lib/solargraph/doc_map.rb | 40 ++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index f7bca7191..465dab558 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -4,6 +4,8 @@ module Solargraph # A collection of pins generated from required gems. # class DocMap + include Logging + # @return [Array] attr_reader :requires @@ -84,7 +86,7 @@ def try_cache gemspec self.class.gems_in_memory[gemspec] = gempins @pins.concat gempins else - Solargraph.logger.debug "No pin cache for #{gemspec.name} #{gemspec.version}" + logger.debug "No pin cache for #{gemspec.name} #{gemspec.version}" @uncached_gemspecs.push gemspec end end @@ -94,11 +96,11 @@ def try_cache gemspec def try_stdlib_map path map = RbsMap::StdlibMap.load(path) if map.resolved? - Solargraph.logger.debug "Loading stdlib pins for #{path}" + logger.debug { "Loading stdlib pins for #{path}" } @pins.concat map.pins else # @todo Temporarily ignoring unresolved `require 'set'` - Solargraph.logger.debug "Require path #{path} could not be resolved" unless path == 'set' + logger.debug { "Require path #{path} could not be resolved" } unless path == 'set' end end @@ -107,21 +109,37 @@ def try_stdlib_map path def try_gem_in_memory gemspec gempins = DocMap.gems_in_memory[gemspec] return false unless gempins - Solargraph.logger.debug "Found #{gemspec.name} #{gemspec.version} in memory" + logger.debug { "Found #{gemspec.name} #{gemspec.version} in memory" } @pins.concat gempins true end # @param gemspec [Gem::Specification] def update_from_collection gemspec, gempins - return gempins unless @rbs_path && File.directory?(@rbs_path) - return gempins if RbsMap.new(gemspec.name, gemspec.version).resolved? + unless @rbs_path && File.directory?(@rbs_path) + logger.debug { "DocMap#update_from_collection: No collection" } + return gempins + end + rbs_map = RbsMap.new(gemspec.name, gemspec.version) + if rbs_map.resolved? + logger.info { "DocMap#update_from_collection: Resolved #{gemspec.name} to RBS exported by gem" } + return GemPins.combine(gempins, rbs_map) + end rbs_map = RbsMap.new(gemspec.name, gemspec.version, directories: [@rbs_path]) - return gempins unless rbs_map.resolved? + if rbs_map.resolved? + logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" + return GemPins.combine(gempins, rbs_map) + end + + rbs_map = RbsMap.new(gemspec.name, nil, directories: [@rbs_path]) + if rbs_map.resolved? + logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" + return GemPins.combine(gempins, rbs_map) + end - Solargraph.logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" - GemPins.combine(gempins, rbs_map) + logger.debug { "DocMap#update_from_collection: No collection found for #{gemspec.name}" } + gempins end # @param path [String] @@ -141,8 +159,8 @@ def resolve_path_to_gemspec path file = "lib/#{path}.rb" gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } rescue Gem::MissingSpecError - Solargraph.logger.debug "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" - nil + logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" } + [] end end gemspec_or_preference gemspec From 6472875ff850f5fa38160426db8a4e76e23b6814 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 May 2025 07:47:57 -0400 Subject: [PATCH 026/116] GemPins pin merging improvements Put responsibility for intelligently merging multiple pins for the same underlying item (e.g. from different sources like YARD and RBS) into the Pin class hierarchy. --- .github/workflows/typecheck.yml | 2 +- lib/solargraph.rb | 19 +++ lib/solargraph/api_map.rb | 28 ++-- lib/solargraph/api_map/index.rb | 3 + lib/solargraph/api_map/store.rb | 3 +- lib/solargraph/equality.rb | 5 + lib/solargraph/gem_pins.rb | 65 ++++++--- lib/solargraph/location.rb | 4 + lib/solargraph/pin/base.rb | 163 ++++++++++++++++++---- lib/solargraph/pin/base_variable.rb | 17 +-- lib/solargraph/pin/callable.rb | 67 +++++++++ lib/solargraph/pin/closure.rb | 12 ++ lib/solargraph/pin/local_variable.rb | 13 +- lib/solargraph/pin/method.rb | 197 ++++++++++++++++++++++++--- lib/solargraph/pin/parameter.rb | 36 ++++- lib/solargraph/pin/signature.rb | 26 ++++ lib/solargraph/source_map.rb | 17 --- 17 files changed, 564 insertions(+), 113 deletions(-) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 5b1b5e151..9c827049f 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -31,4 +31,4 @@ jobs: - name: Install gems run: bundle install - name: Typecheck self - run: bundle exec solargraph typecheck --level typed + run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 352b0eaad..b766bf052 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -52,6 +52,25 @@ class InvalidRubocopVersionError < RuntimeError; end dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') + # @param type [Symbol] Type of assert. Not used yet, but may be + # used in the future to allow configurable asserts mixes for + # different situations. + def self.asserts_on?(type) + @asserts_on ||= if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? + false + elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' + true + else + logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" + false + end + end + + def self.assert_or_log(type, msg) + raise msg if asserts_on?(type) + logger.info msg + end + # A convenience method for Solargraph::Logging.logger. # # @return [Logger] diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 9443e8529..036927388 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -600,6 +600,19 @@ def type_include?(host_ns, module_ns) store.get_includes(host_ns).map { |inc_tag| ComplexType.parse(inc_tag).name }.include?(module_ns) end + # @param pins [Enumerable] + # @param visibility [Enumerable] + # @return [Array] + def resolve_method_aliases pins, visibility = [:public, :private, :protected] + with_resolved_aliases = pins.map do |pin| + resolved = resolve_method_alias(pin) + next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) + resolved + end.compact + logger.debug { "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" } + GemPins.combine_method_pins_by_path(with_resolved_aliases) + end + private # A hash of source maps with filename keys. @@ -802,17 +815,6 @@ def prefer_non_nil_variables pins result + nil_pins end - # @param pins [Enumerable] - # @param visibility [Enumerable] - # @return [Array] - def resolve_method_aliases pins, visibility = [:public, :private, :protected] - pins.map do |pin| - resolved = resolve_method_alias(pin) - next pin if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) - resolved - end.compact - end - # @param pin [Pin::MethodAlias, Pin::Base] # @return [Pin::Method] def resolve_method_alias pin @@ -835,7 +837,9 @@ def resolve_method_alias pin generics: origin.generics, return_type: origin.return_type, } - Pin::Method.new **args + out = Pin::Method.new **args + logger.debug { "ApiMap#resolve_method_alias(pin=#{pin}) - returning #{out} from #{origin}" } + out end include Logging diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index f1b68f41c..dcb273071 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -156,11 +156,14 @@ def map_overrides # @param tag [YARD::Tags::Tag] # @return [void] def redefine_return_type pin, tag + # @todo can this be made to not mutate existing pins and use + # proxy() / proxy_with_signatures() instead? return unless pin && tag.tag_name == 'return' pin.instance_variable_set(:@return_type, ComplexType.try_parse(tag.type)) pin.signatures.each do |sig| sig.instance_variable_set(:@return_type, ComplexType.try_parse(tag.type)) end + pin.reset_generated! end end end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 5f893ed77..67495cdad 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -61,9 +61,10 @@ def get_constants fqns, visibility = [:public] # @param visibility [Array] # @return [Enumerable] def get_methods fqns, scope: :instance, visibility: [:public] - namespace_children(fqns).select do |pin| + all_pins = namespace_children(fqns).select do |pin| pin.is_a?(Pin::Method) && pin.scope == scope && visibility.include?(pin.visibility) end + GemPins.combine_method_pins_by_path(all_pins) end # @param fqns [String] diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index 0667efacd..8c5611627 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -29,5 +29,10 @@ def freeze equality_fields.each(&:freeze) super end + + def <=>(other) + return nil unless other.is_a?(self.class) + equality_fields <=> other.equality_fields + end end end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index fb23655ed..487b93e0f 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -7,6 +7,10 @@ module Solargraph # documentation. # module GemPins + class << self + include Logging + end + # Build an array of pins from a gem specification. The process starts with # YARD, enhances the resulting pins with RBS definitions, and appends RBS # pins that don't exist in the YARD mapping. @@ -19,34 +23,55 @@ def self.build(gemspec) combine yard_pins, rbs_map end + def self.combine_method_pins_by_path(pins) + # @type [Hash{::Array(String, String) => ::Array}] + by_name_and_class = {} + pins.each do |pin| + by_name_and_class[[pin.name, pin.path, pin.class]] ||= [] + by_name_and_class[[pin.name, pin.path, pin.class]].push pin + end + by_name_and_class.transform_values! do |pins| + method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } + [GemPins.combine_method_pins(*method_pins)].compact + alias_pins + end + by_name_and_class.values.flatten(1) + end + + def self.combine_method_pins(*pins) + out = pins.reduce(nil) do |memo, pin| + raise "sent wrong type of pin: #{pin.inspect}" unless pin.class == Pin::Method + next pin if memo.nil? + memo.combine_with(pin) + end + logger.debug { "GemPins.combine_method_pins(pins.length=#{pins.length}, pins=#{pins}) => #{out.inspect}" } + out + end + # @param yard_pins [Array] # @param rbs_map [RbsMap] # @return [Array] def self.combine(yard_pins, rbs_map) in_yard = Set.new - combined = yard_pins.map do |yard| - in_yard.add yard.path - next yard unless yard.is_a?(Pin::Method) + rbs_api_map = Solargraph::ApiMap.new(pins: rbs_map.pins) + combined = yard_pins.map do |yard_pin| + next yard_pin unless yard_pin.class == Pin::Method + + in_yard.add yard_pin.path - rbs = rbs_map.path_pin(yard.path, Pin::Method) - next yard unless rbs + rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first + unless rbs_pin + logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" } + next yard_pin + end - # @sg-ignore - yard.class.new( - location: yard.location, - closure: yard.closure, - name: yard.name, - comments: yard.comments, - scope: yard.scope, - parameters: rbs.parameters, - generics: rbs.generics, - node: yard.node, - signatures: yard.signatures, - return_type: best_return_type(rbs.return_type, yard.return_type) - ) + out = combine_method_pins(rbs_pin, yard_pin) + logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" } + out + end + in_rbs_only = rbs_map.pins.select do |pin| + pin.path.nil? || !in_yard.include?(pin.path) end - in_rbs = rbs_map.pins.reject { |pin| in_yard.include?(pin.path) } - combined + in_rbs + combined + in_rbs_only end class << self diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index f98f6b82d..73dda87f5 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -25,6 +25,10 @@ def initialize filename, range [filename, range] end + def rbs? + filename.end_with?('.rbs') + end + # @param location [self] def contain? location range.contain?(location.range.start) && range.contain?(location.range.ending) && filename == location.filename diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index b81b28df3..cfe4c9afd 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -8,6 +8,8 @@ class Base include Common include Conversions include Documenting + include Logging + # @return [YARD::CodeObjects::Base] attr_reader :code_object @@ -36,12 +38,141 @@ def presence_certain? # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] # @param comments [String] - def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '' + def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '', docstring: nil, source: nil, directives: nil @location = location @type_location = type_location @closure = closure @name = name @comments = comments + @source = source + @identity = nil + @docstring = docstring + @directives = directives + end + + # @param other [self] + # @param attrs [Hash{Symbol => Object}] + # + # @return [self] + def combine_with(other, attrs={}) + raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class + type_location = choose(other, :type_location) + location = choose(other, :location) || type_location + new_attrs = { + location: location, + type_location: type_location, + closure: choose(other, :closure), + name: choose(other, :name), + comments: choose(other, :comments), + source: :combined, + docstring: choose(other, :docstring), + directives: combine_directives(other), + }.merge(attrs) + logger.debug { "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{self.comments}" } + out = self.class.new(**new_attrs) + out.reset_generated! + out + end + + def combine_directives(other) + return self.directives if other.directives.empty? + return other.directives if directives.empty? + [directives + other.directives].uniq + end + + def reset_generated! + # @return_type doesn't go here as subclasses tend to assign it + # themselves in constructors, and they will deal with setting + # it in any methods that call this + # + # @docstring also doesn't go here, as there is code which + # directly manipulates docstring without editing comments + # (e.g., Api::Map::Store#index processes overrides that way + # + # Same with @directives, @macros, @maybe_directives, which + # regenerate docstring + @deprecated = nil + reset_conversions + end + + # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" + protected def equality_fields + [name, location, type_location, closure, source] + end + + def combine_return_type(other) + if return_type.undefined? + other.return_type + elsif other.return_type.undefined? + return_type + else + all_items = return_type.items + other.return_type.items + if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_tag == context.rooted_tag } + # assume this was a declaration that should have said 'self' + all_items.delete_if { |item| item.rooted_tag == context.rooted_tag } + end + ComplexType.new(all_items) + end + end + + def <=>(p1) + return nil unless p1.is_a?(self.class) + return 0 if self == p1 + equality_fields <=> equality_fields + end + + # when choices are arbitrary, make sure the choice is consistent + # + # @param other [Pin::Base] + # @param attr [::Symbol] + # + # @return [Object, nil] + def choose(other, attr) + results = [self, other].map(&attr).compact + # true and false are different classes and can't be sorted + return true if results.any? { |r| r == true || r == false } + results.min + rescue + STDERR.puts("Problem handling #{attr} for \n#{self.inspect}\n and \n#{other.inspect}\n\n#{self.send(attr).inspect} vs #{other.send(attr).inspect}") + raise + end + + def choose_node(other, attr) + if other.object_id < attr.object_id + other.send(attr) + else + send(attr) + end + end + + def prefer_rbs_location(other, attr) + if rbs_location? && !other.rbs_location? + self.send(attr) + elsif !rbs_location? && other.rbs_location? + other.send(attr) + else + choose(other, attr) + end + end + + def rbs_location? + type_location&.rbs? + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [Object, nil] + def assert_same(other, attr) + val1 = send(attr) + val2 = other.send(attr) + return val1 if val1 == val2 + msg = "Inconsistent #{attr.inspect} values from \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}" + if ENV['DEBUG_PINS'] + raise msg + end + logger.info msg + val1 end # @return [String] @@ -152,13 +283,13 @@ def return_type # @return [YARD::Docstring] def docstring - parse_comments unless defined?(@docstring) + parse_comments unless @docstring @docstring ||= Solargraph::Source.parse_docstring('').to_docstring end # @return [::Array] def directives - parse_comments unless defined?(@directives) + parse_comments unless @directives @directives end @@ -176,7 +307,7 @@ def macros # # @return [Boolean] def maybe_directives? - return !@directives.empty? if defined?(@directives) + return !@directives.empty? if defined?(@directives) && @directives @maybe_directives ||= comments.include?('@!') end @@ -215,26 +346,6 @@ def infer api_map probe api_map end - # Try to merge data from another pin. Merges are only possible if the - # pins are near matches (see the #nearly? method). The changes should - # not have any side effects on the API surface. - # - # @param pin [Pin::Base] The pin to merge into this one - # @return [Boolean] True if the pins were merged - def try_merge! pin - return false unless nearly?(pin) - @location = pin.location - @closure = pin.closure - return true if comments == pin.comments - @comments = pin.comments - @docstring = pin.docstring - @return_type = pin.return_type - @documentation = nil - @deprecated = nil - reset_conversions - true - end - def proxied? @proxied ||= false end @@ -318,6 +429,10 @@ def inspect # @return [ComplexType] attr_writer :return_type + attr_writer :docstring + + attr_writer :directives + private # @return [void] diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index ac84378d1..1cbd628d7 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -21,6 +21,15 @@ def initialize assignment: nil, return_type: nil, **splat @return_type = return_type end + def combine_with(other, attrs={}) + attrs.merge({ + assignment: assert_same(other, :assignment), + mass_assignment: assert_same(other, :mass_assignment), + return_type: combine_return_type(other), + }) + super(other, attrs) + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::VARIABLE end @@ -95,14 +104,6 @@ def == other assignment == other.assignment end - # @param pin [self] - def try_merge! pin - return false unless super - @assignment = pin.assignment - @return_type = pin.return_type - true - end - def type_desc "#{super} = #{assignment&.type.inspect}" end diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index b09926897..02f28ae84 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -21,11 +21,61 @@ def initialize block: nil, return_type: nil, parameters: [], **splat @parameters = parameters end + def method_namespace + closure.namespace + end + + def combine_blocks(other) + if block.nil? + other.block + elsif other.block.nil? + block + else + choose(other, :block) + end + end + + # @param other [self] + # @param attrs [Hash{Symbol => Object}] + # + # @return [self] + def combine_with(other, attrs={}) + new_attrs = { + block: combine_blocks(other), + return_type: combine_return_type(other), + }.merge(attrs) + new_attrs[:parameters] = choose_parameters(other) unless new_attrs.key?(:parameters) + super(other, new_attrs) + end + # @return [::Array] def parameter_names @parameter_names ||= parameters.map(&:name) end + def generics + [] + end + + def choose_parameters(other) + raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity + parameters.each_with_index.map do |param, i| + param.combine_with(other.parameters[i]) + end + end + + def blockless_parameters + if parameters.last&.block? + parameters[0..-2] + else + parameters + end + end + + def arity + [generics, blockless_parameters.map(&:arity_decl), block&.arity] + end + # @param generics_to_resolve [Enumerable] # @param arg_types [Array, nil] # @param return_type_context [ComplexType, nil] @@ -57,6 +107,23 @@ def resolve_generics_from_context(generics_to_resolve, callable end + def typify api_map + type = super + return type if type.defined? + if method_name.end_with?('?') + logger.debug { "Callable#typify(self=#{self}) => Boolean (? suffix)" } + ComplexType::BOOLEAN + else + logger.debug { "Callable#typify(self=#{self}) => undefined" } + ComplexType::UNDEFINED + end + end + + def method_name + raise "closure was nil in #{self.inspect}" if closure.nil? + @method_name ||= closure.name + end + # @param generics_to_resolve [::Array] # @param arg_types [Array, nil] # @param return_type_context [ComplexType, nil] diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index e1b022041..6fb05eca7 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -14,6 +14,18 @@ def initialize scope: :class, generics: nil, **splat @generics = generics end + # @param other [self] + # @param attrs [Hash{Symbol => Object}] + # + # @return [self] + def combine_with(other, attrs={}) + new_attrs = { + scope: assert_same(other, :scope), + generics: generics.empty? ? other.generics : generics, + }.merge(attrs) + super(other, new_attrs) + end + def context @context ||= begin result = super diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index b107dbfb5..c680bebd0 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -21,11 +21,14 @@ def initialize assignment: nil, presence: nil, presence_certain: false, **splat @presence_certain = presence_certain end - # @param pin [self] - def try_merge! pin - return false unless super - @presence = pin.presence - true + def combine_with(other, attrs={}) + new_attrs = { + assignment: assert_same(other, :assignment), + presence_certain: assert_same(other, :presence_certain?), + }.merge(attrs) + new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) + + super(other, new_attrs) end # @param other_closure [Pin::Closure] diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 5c94647e6..b3b50b4c1 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -31,6 +31,129 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @anon_splat = anon_splat end + def combine_all_signature_pins(*signature_pins) + by_arity = {} + signature_pins.each do |signature_pin| + by_arity[signature_pin.arity] ||= [] + by_arity[signature_pin.arity] << signature_pin + end + by_arity.transform_values! do |same_arity_pins| + same_arity_pins.reduce(nil) do |memo, signature| + next signature if memo.nil? + memo.combine_with(signature) + end + end + by_arity.values.flatten + end + + def combine_visibility(other) + if dodgy_visibility_source? && !other.dodgy_visibility_source? + other.visibility + elsif other.dodgy_visibility_source? && !dodgy_visibility_source? + visibility + else + assert_same(other, :visibility) + end + end + + def combine_with(other, attrs = {}) + sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) + new_attrs = { + visibility: combine_visibility(other), + explicit: explicit? || other.explicit?, + block: combine_blocks(other), + node: choose_node(other, :node), + attribute: prefer_rbs_location(other, :attribute?), + parameters: choose(other, :parameters), + signatures: sigs, + anon_splat: assert_same(other, :anon_splat?), + return_type: nil # pulled from signatures on first call + }.merge(attrs) + super(other, new_attrs) + end + + def combine_all_signature_pins(*signature_pins) + by_arity = {} + signature_pins.each do |signature_pin| + by_arity[signature_pin.arity] ||= [] + by_arity[signature_pin.arity] << signature_pin + end + by_arity.transform_values! do |same_arity_pins| + same_arity_pins.reduce(nil) do |memo, signature| + next signature if memo.nil? + memo.combine_with(signature) + end + end + by_arity.values.flatten + end + + def combine_visibility(other) + if dodgy_visibility_source? && !other.dodgy_visibility_source? + other.visibility + elsif other.dodgy_visibility_source? && !dodgy_visibility_source? + visibility + else + assert_same(other, :visibility) + end + end + + def combine_with(other, attrs = {}) + sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) + new_attrs = { + visibility: combine_visibility(other), + explicit: explicit? || other.explicit?, + block: combine_blocks(other), + node: choose_node(other, :node), + attribute: prefer_rbs_location(other, :attribute?), + parameters: choose(other, :parameters), + signatures: sigs, + anon_splat: assert_same(other, :anon_splat?), + return_type: nil # pulled from signatures on first call + }.merge(attrs) + super(other, new_attrs) + end + + def combine_all_signature_pins(*signature_pins) + by_arity = {} + signature_pins.each do |signature_pin| + by_arity[signature_pin.arity] ||= [] + by_arity[signature_pin.arity] << signature_pin + end + by_arity.transform_values! do |same_arity_pins| + same_arity_pins.reduce(nil) do |memo, signature| + next signature if memo.nil? + memo.combine_with(signature) + end + end + by_arity.values.flatten + end + + def combine_visibility(other) + if dodgy_visibility_source? && !other.dodgy_visibility_source? + other.visibility + elsif other.dodgy_visibility_source? && !dodgy_visibility_source? + visibility + else + assert_same(other, :visibility) + end + end + + def combine_with(other, attrs = {}) + sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) + new_attrs = { + visibility: combine_visibility(other), + explicit: explicit? || other.explicit?, + block: combine_blocks(other), + node: choose_node(other, :node), + attribute: prefer_rbs_location(other, :attribute?), + parameters: choose(other, :parameters), + signatures: sigs, + anon_splat: assert_same(other, :anon_splat?), + return_type: nil # pulled from signatures on first call + }.merge(attrs) + super(other, new_attrs) + end + def == other super && other.node == node end @@ -42,11 +165,17 @@ def transform_types(&transform) sig.transform_types(&transform) end m.block = block&.transform_types(&transform) - m.signature_help = nil - m.documentation = nil + m.reset_generated! m end + def reset_generated! + super + return_type = nil unless signatures.empty? + signature_help = nil + documentation = nil + end + def all_rooted? super && parameters.all?(&:all_rooted?) && (!block || block&.all_rooted?) && signatures.all?(&:all_rooted?) end @@ -55,8 +184,7 @@ def all_rooted? # @return [Pin::Method] def with_single_signature(signature) m = proxy signature.return_type - m.signature_help = nil - m.documentation = nil + m.reset_generated! # @todo populating the single parameters/return_type/block # arguments here seems to be needed for some specs to pass, # even though we have a signature with the same information. @@ -120,9 +248,9 @@ 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) + block = Signature.new(generics: generics, parameters: yield_parameters, return_type: yield_return_type, source: source, closure: self) end - Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block) + Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, source: source, closure: self) end # @return [::Array] @@ -137,6 +265,12 @@ def signatures end end + def proxy_with_signatures return_type + out = proxy return_type + out.signatures = out.signatures.map { |sig| sig.proxy return_type } + out + end + # @return [String, nil] def detail # This property is not cached in an instance variable because it can @@ -188,12 +322,25 @@ def path @path ||= "#{namespace}#{(scope == :instance ? '#' : '.')}#{name}" end + def method_name + name + end + def typify api_map + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" } decl = super - return decl unless decl.undefined? + unless decl.undefined? + logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context}) => #{decl.rooted_tags.inspect} - decl found" } + return decl + end type = see_reference(api_map) || typify_from_super(api_map) - return type.qualify(api_map, namespace) unless type.nil? - name.end_with?('?') ? ComplexType::BOOLEAN : ComplexType::UNDEFINED + logger.debug { "Method#typify(self=#{self}) - type=#{type&.rooted_tags.inspect}" } + unless type.nil? + qualified = type.qualify(api_map, namespace) + logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } + return qualified + end + super end # @sg-ignore @@ -279,14 +426,6 @@ def probe api_map attribute? ? infer_from_iv(api_map) : infer_from_return_nodes(api_map) end - # @param pin [Pin::Method] - def try_merge! pin - return false unless super - @node = pin.node - @resolved_ref_tag = false - true - end - # @return [::Array] def overloads # Ignore overload tags with nil parameters. If it's not an array, the @@ -306,7 +445,9 @@ def overloads return_type: param_type_from_name(tag, src.first) ) end, - return_type: ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types)) + closure: self, + return_type: ComplexType.try_parse(*tag.docstring.tags(:return).flat_map(&:types)), + source: :overloads, ) end @overloads @@ -339,6 +480,11 @@ def resolve_ref_tag api_map self end + # @param api_map [ApiMap] + def rest_of_stack api_map + api_map.get_method_stack(method_namespace, method_name, scope: scope).reject { |pin| pin.path == path } + end + protected attr_writer :block @@ -349,6 +495,13 @@ def resolve_ref_tag api_map attr_writer :documentation + def dodgy_visibility_source? + # as of 2025-03-12, the RBS generator used for + # e.g. activesupport did not understand 'private' markings + # inside 'class << self' blocks, but YARD did OK at it + source == :rbs && scope == :class && type_location&.filename&.include?('generated') + end + private # @param name [String] @@ -409,10 +562,14 @@ def see_reference api_map resolve_reference match[1], api_map end + def method_namespace + namespace + end + # @param api_map [ApiMap] # @return [ComplexType, nil] def typify_from_super api_map - stack = api_map.get_method_stack(namespace, name, scope: scope).reject { |pin| pin.path == path } + stack = rest_of_stack api_map return nil if stack.empty? stack.each do |pin| return pin.return_type unless pin.return_type.undefined? @@ -523,6 +680,8 @@ def concat_example_tags protected attr_writer :signatures + + attr_writer :return_type end end end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 71a247ac2..7c8074a99 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -17,6 +17,15 @@ def initialize decl: :arg, asgn_code: nil, **splat @decl = decl end + def combine_with(other, attrs={}) + new_attrs = { + decl: assert_same(other, :decl), + presence: choose(other, :presence), + asgn_code: choose(other, :asgn_code), + }.merge(attrs) + super(other, new_attrs) + end + def keyword? [:kwarg, :kwoptarg].include?(decl) end @@ -25,6 +34,27 @@ def kwrestarg? decl == :kwrestarg || (assignment && [:HASH, :hash].include?(assignment.type)) end + def arity_decl + name = (name || '(anon)') + type = (return_type&.to_rbs || 'untyped') + case decl + when :arg + "" + when :optarg + "?" + when :kwarg + "#{name}:" + when :kwoptarg + "?#{name}:" + when :restarg + "*" + when :kwrestarg + "**" + else + "(unknown decl: #{decl})" + end + end + def arg? decl == :arg end @@ -119,12 +149,6 @@ def documentation tag.text end - # @param pin [Pin::Parameter] - def try_merge! pin - return false unless super && closure == pin.closure - true - end - private # @return [YARD::Tags::Tag, nil] diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index da6f6a385..886f14705 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -12,6 +12,32 @@ def generics def identity @identity ||= "signature#{object_id}" end + + attr_writer :closure + + def typify api_map + if return_type.defined? + qualified = return_type.qualify(api_map, closure.namespace) + logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } + return qualified + end + return ComplexType::UNDEFINED if closure.nil? + return ComplexType::UNDEFINED unless closure.is_a?(Pin::Method) + method_stack = closure.rest_of_stack api_map + logger.debug { "Signature#typify(self=#{self}) - method_stack: #{method_stack}" } + method_stack.each do |pin| + sig = pin.signatures.find { |s| s.arity == self.arity } + next unless sig + unless sig.return_type.undefined? + qualified = sig.return_type.qualify(api_map, closure.namespace) + logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" } + return qualified + end + end + out = super + logger.debug { "Signature#typify(self=#{self}) => #{out}" } + out + end end end end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 663e6f406..84b3a4bcc 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -118,23 +118,6 @@ def locate_block_pin line, character _locate_pin line, character, Pin::Namespace, Pin::Method, Pin::Block end - # @todo Candidate for deprecation - # - # @param other_map [SourceMap] - # @return [Boolean] - def try_merge! other_map - return false if pins.length != other_map.pins.length || locals.length != other_map.locals.length || requires.map(&:name).uniq.sort != other_map.requires.map(&:name).uniq.sort - - pins.each_index do |i| - return false unless pins[i].try_merge!(other_map.pins[i]) - end - locals.each_index do |i| - return false unless locals[i].try_merge!(other_map.locals[i]) - end - @source = other_map.source - true - end - # @param name [String] # @return [Array] def references name From 8142fa0f038b48b50feab87e0d52012504b904bf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 May 2025 08:57:27 -0400 Subject: [PATCH 027/116] GemPins pin merging improvements --- lib/solargraph/pin/base.rb | 59 +++++++++++++++++++++--- lib/solargraph/pin/method.rb | 82 --------------------------------- lib/solargraph/pin/parameter.rb | 5 ++ spec/pin/local_variable_spec.rb | 25 ++++++++-- spec/spec_helper.rb | 11 +++++ 5 files changed, 89 insertions(+), 93 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index cfe4c9afd..542cd3c8d 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -58,11 +58,12 @@ def combine_with(other, attrs={}) raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class type_location = choose(other, :type_location) location = choose(other, :location) || type_location + combined_name = combine_name(other) new_attrs = { location: location, type_location: type_location, - closure: choose(other, :closure), - name: choose(other, :name), + name: combined_name, + closure: choose_pin_attr_with_same_name(other, :closure), comments: choose(other, :comments), source: :combined, docstring: choose(other, :docstring), @@ -80,6 +81,14 @@ def combine_directives(other) [directives + other.directives].uniq end + def combine_name(other) + if needs_consistent_name? || other.needs_consistent_name? + assert_same(other, :name) + else + choose(other, :name) + end + end + def reset_generated! # @return_type doesn't go here as subclasses tend to assign it # themselves in constructors, and they will deal with setting @@ -95,6 +104,10 @@ def reset_generated! reset_conversions end + def needs_consistent_name? + true + end + # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" protected def equality_fields [name, location, type_location, closure, source] @@ -167,14 +180,46 @@ def assert_same(other, attr) val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 - msg = "Inconsistent #{attr.inspect} values from \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}" - if ENV['DEBUG_PINS'] - raise msg - end - logger.info msg + Solargraph.assert_or_log(:combine_with, + "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") val1 end + def choose_pin_attr_with_same_name(other, attr) + val1 = send(attr) + val2 = other.send(attr) + if val1&.name != val2&.name + Solargraph.assert_or_log(:combine_with, + "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") + return nil + end + [val1, val2].min + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [Pin::Base, nil] + def assert_combined_pin(other, attr) + val1 = send(attr) + val2 = other.send(attr) + if val1.nil? && val2.nil? + return nil + end + unless val1 && val2 + Solargraph.assert_or_log(:combine_with, + "Inconsistent #{attr.inspect} values from \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") + return val1 || val2 + end + if val1.object_id == self.object_id || val2.object_id == self.object_id + Solargraph.assert_or_log(:combine_with, + "Loop found: #{val1.inspect} or #{val2.inspect} may be same as self - #{inspect}") + return nil + end + + val1.combine_with(val2) + end + # @return [String] def comments @comments ||= '' diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index b3b50b4c1..3dc2fe92f 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -72,88 +72,6 @@ def combine_with(other, attrs = {}) super(other, new_attrs) end - def combine_all_signature_pins(*signature_pins) - by_arity = {} - signature_pins.each do |signature_pin| - by_arity[signature_pin.arity] ||= [] - by_arity[signature_pin.arity] << signature_pin - end - by_arity.transform_values! do |same_arity_pins| - same_arity_pins.reduce(nil) do |memo, signature| - next signature if memo.nil? - memo.combine_with(signature) - end - end - by_arity.values.flatten - end - - def combine_visibility(other) - if dodgy_visibility_source? && !other.dodgy_visibility_source? - other.visibility - elsif other.dodgy_visibility_source? && !dodgy_visibility_source? - visibility - else - assert_same(other, :visibility) - end - end - - def combine_with(other, attrs = {}) - sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) - new_attrs = { - visibility: combine_visibility(other), - explicit: explicit? || other.explicit?, - block: combine_blocks(other), - node: choose_node(other, :node), - attribute: prefer_rbs_location(other, :attribute?), - parameters: choose(other, :parameters), - signatures: sigs, - anon_splat: assert_same(other, :anon_splat?), - return_type: nil # pulled from signatures on first call - }.merge(attrs) - super(other, new_attrs) - end - - def combine_all_signature_pins(*signature_pins) - by_arity = {} - signature_pins.each do |signature_pin| - by_arity[signature_pin.arity] ||= [] - by_arity[signature_pin.arity] << signature_pin - end - by_arity.transform_values! do |same_arity_pins| - same_arity_pins.reduce(nil) do |memo, signature| - next signature if memo.nil? - memo.combine_with(signature) - end - end - by_arity.values.flatten - end - - def combine_visibility(other) - if dodgy_visibility_source? && !other.dodgy_visibility_source? - other.visibility - elsif other.dodgy_visibility_source? && !dodgy_visibility_source? - visibility - else - assert_same(other, :visibility) - end - end - - def combine_with(other, attrs = {}) - sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) - new_attrs = { - visibility: combine_visibility(other), - explicit: explicit? || other.explicit?, - block: combine_blocks(other), - node: choose_node(other, :node), - attribute: prefer_rbs_location(other, :attribute?), - parameters: choose(other, :parameters), - signatures: sigs, - anon_splat: assert_same(other, :anon_splat?), - return_type: nil # pulled from signatures on first call - }.merge(attrs) - super(other, new_attrs) - end - def == other super && other.node == node end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 7c8074a99..8fa1da7df 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -34,6 +34,11 @@ def kwrestarg? decl == :kwrestarg || (assignment && [:HASH, :hash].include?(assignment.type)) end + def needs_consistent_name? + keyword? + end + + def arity_decl name = (name || '(anon)') type = (return_type&.to_rbs || 'untyped') diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 02665a64c..3ab595d5f 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::Pin::LocalVariable do - it "merges presence changes" do + xit "merges presence changes so that [not currently used]" do map1 = Solargraph::SourceMap.load_string(%( class Foo foo = 'foo' @@ -7,6 +7,9 @@ class Foo end )) pin1 = map1.locals.first + expect(pin1.presence.start.to_hash).to eq({ line: 2, character: 8 }) + expect(pin1.presence.ending.to_hash).to eq({ line: 4, character: 9 }) + map2 = Solargraph::SourceMap.load_string(%( class Foo @more = 'more' @@ -15,10 +18,19 @@ class Foo end )) pin2 = map2.locals.first - expect(pin1.try_merge!(pin2)).to be(true) + expect(pin2.presence.start.to_hash).to eq({ line: 3, character: 8 }) + expect(pin2.presence.ending.to_hash).to eq({ line: 5, character: 9 }) + + combined = pin1.combine_with(pin2) + expect(combined).to be_a(Solargraph::Pin::LocalVariable) + + + expect(combined.source).to eq(:combined) + # no choice behavior defined yet - if/when this is to be used, we + # should indicate which one should override in the range situation end - it "does not merge namespace changes" do + it "asserts on attempt to merge namespace changes" do map1 = Solargraph::SourceMap.load_string(%( class Foo foo = 'foo' @@ -31,6 +43,11 @@ class Bar end )) pin2 = map2.locals.first - expect(pin1.try_merge!(pin2)).to be(false) + # set env variable 'FOO' to 'true' in block + + with_env_var('SOLARGRAPH_ASSERTS', 'on') do + expect(Solargraph.asserts_on?(:combine_with)).to be true + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :name values/) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5e0385b74..faba8172e 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,3 +8,14 @@ require 'solargraph' # Suppress logger output in specs (if possible) Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) + +def with_env_var(name, value) + old_value = ENV[name] # Store the old value + ENV[name] = value # Set to new value + + begin + yield # Execute the block + ensure + ENV[name] = old_value # Restore the old value + end +end From de8506aeed8d3d0f5b187c2427ba1342df1a22ce Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 May 2025 14:31:35 -0400 Subject: [PATCH 028/116] GemPins pin merging improvements --- lib/solargraph/pin/base.rb | 11 ++++++- spec/pin/base_spec.rb | 28 ++++++----------- spec/pin/local_variable_spec.rb | 2 +- spec/pin/parameter_spec.rb | 13 ++------ spec/source_map_spec.rb | 54 --------------------------------- 5 files changed, 22 insertions(+), 86 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 542cd3c8d..e7acf20f1 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -64,17 +64,26 @@ def combine_with(other, attrs={}) type_location: type_location, name: combined_name, closure: choose_pin_attr_with_same_name(other, :closure), - comments: choose(other, :comments), + comments: choose_longer(other, :comments), source: :combined, docstring: choose(other, :docstring), directives: combine_directives(other), }.merge(attrs) + assert_same(other, :macros) logger.debug { "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{self.comments}" } out = self.class.new(**new_attrs) out.reset_generated! out end + def choose_longer(other, attr) + val1 = send(attr) + val2 = other.send(attr) + return val1 if val1 == val2 + return val2 if val1.nil? + val1.length > val2.length ? val1 : val2 + end + def combine_directives(other) return self.directives if other.directives.empty? return other.directives if directives.empty? diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index 810fe5ce0..c571c9a91 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -2,33 +2,23 @@ let(:zero_location) { Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 0, 0)) } let(:one_location) { Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 1, 0)) } - it "merges pins with location changes" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo') - pin2 = Solargraph::Pin::Base.new(location: one_location, name: 'Foo') - expect(pin1.try_merge!(pin2)).to eq(true) - expect(pin1.location).to eq(one_location) - end - - it "merges pins with comment changes" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class') - merge_comment = 'A modified Foo class' - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: merge_comment) - expect(pin1.try_merge!(pin2)).to eq(true) - expect(pin1.comments).to eq(merge_comment) - end - - it "will not merge pins with directive changes" do + it "will not combine pins with directive changes" do pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class') pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro') expect(pin1.nearly?(pin2)).to be(false) - expect(pin1.try_merge!(pin2)).to be(false) + # enable asserts + with_env_var('SOLARGRAPH_ASSERTS', 'on') do + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) + end end - it "will not merge pins with different directives" do + it "will not combine pins with different directives" do pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro') pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro other') expect(pin1.nearly?(pin2)).to be(false) - expect(pin1.try_merge!(pin2)).to be(false) + with_env_var('SOLARGRAPH_ASSERTS', 'on') do + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) + end end it "sees tag differences as not near or equal" do diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 3ab595d5f..b4f676157 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -47,7 +47,7 @@ class Bar with_env_var('SOLARGRAPH_ASSERTS', 'on') do expect(Solargraph.asserts_on?(:combine_with)).to be true - expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :name values/) + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) end end end diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 9010d1524..f2553a8ec 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -193,21 +193,12 @@ def baz; end expect(type.namespace).to eq('Foo::Bar') end - it 'merges near equivalents' do + it 'uses longer comment while combining compatible parameters' do loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 0, 0)) block = Solargraph::Pin::Block.new(location: loc, name: 'Foo') pin1 = Solargraph::Pin::Parameter.new(closure: block, name: 'bar') pin2 = Solargraph::Pin::Parameter.new(closure: block, name: 'bar', comments: 'a comment') - expect(pin1.try_merge!(pin2)).to be(true) - end - - it 'does not merge block parameters from different blocks' do - loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 0, 0)) - block1 = Solargraph::Pin::Block.new(location: loc, name: 'Foo') - block2 = Solargraph::Pin::Block.new(location: loc, name: 'Bar') - pin1 = Solargraph::Pin::Parameter.new(closure: block1, name: 'bar') - pin2 = Solargraph::Pin::Parameter.new(closure: block2, name: 'bar', comments: 'a comment') - expect(pin1.try_merge!(pin2)).to be(false) + expect(pin1.combine_with(pin2).comments).to eq('a comment') end it 'infers undefined types by default' do diff --git a/spec/source_map_spec.rb b/spec/source_map_spec.rb index 247453624..7d835564b 100644 --- a/spec/source_map_spec.rb +++ b/spec/source_map_spec.rb @@ -74,60 +74,6 @@ class Foo expect(pin).to be_a(Solargraph::Pin::Block) end - it "merges comment changes" do - map1 = Solargraph::SourceMap.load_string(%( - class Foo - def bar; end - end - )) - map2 = Solargraph::SourceMap.load_string(%( - class Foo - # My bar method - def bar; end - end - )) - expect(map1.try_merge!(map2)).to be(true) - end - - it "merges require equivalents" do - map1 = Solargraph::SourceMap.load_string("require 'foo'") - map2 = Solargraph::SourceMap.load_string("require 'foo' # Insignificant comment") - expect(map1.try_merge!(map2)).to be(true) - end - - it "does not merge require changes" do - map1 = Solargraph::SourceMap.load_string("require 'foo'") - map2 = Solargraph::SourceMap.load_string("require 'bar'") - expect(map1.try_merge!(map2)).to be(false) - end - - it "merges repaired changes" do - source1 = Solargraph::Source.load_string(%( - list.each do |item| - i - end - )) - updater = Solargraph::Source::Updater.new( - nil, - 2, - [ - Solargraph::Source::Change.new( - Solargraph::Range.from_to(2, 8, 2, 8), - 'f ' - ) - ] - ) - source2 = source1.synchronize(updater) - map1 = Solargraph::SourceMap.map(source1) - pos1 = Solargraph::Position.new(2, 8) - pin1 = map1.pins.select{|p| p.location.range.contain?(pos1)}.first - map2 = Solargraph::SourceMap.map(source2) - expect(map1.try_merge!(map2)).to be(true) - pos2 = Solargraph::Position.new(2, 10) - pin2 = map1.pins.select{|p| p.location.range.contain?(pos2)}.first - expect(pin1).to eq(pin2) - end - it 'scopes local variables correctly from root def blocks' do map = Solargraph::SourceMap.load_string(%( x = 'string' From ded58a6b9d4a9df85ed25f5cd89357acd47e7f9b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 May 2025 14:43:32 -0400 Subject: [PATCH 029/116] GemPins pin merging improvements --- lib/solargraph.rb | 16 ++++++++-------- spec/pin/base_spec.rb | 12 ++++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index b766bf052..07af50508 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -56,14 +56,14 @@ class InvalidRubocopVersionError < RuntimeError; end # used in the future to allow configurable asserts mixes for # different situations. def self.asserts_on?(type) - @asserts_on ||= if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? - false - elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' - true - else - logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" - false - end + if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? + false + elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' + true + else + logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" + false + end end def self.assert_or_log(type, msg) diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index c571c9a91..a66dc52ac 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -3,8 +3,10 @@ let(:one_location) { Solargraph::Location.new('test.rb', Solargraph::Range.from_to(0, 0, 1, 0)) } it "will not combine pins with directive changes" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class') - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro') + pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class', + closure: Solargraph::Pin::ROOT_PIN) + pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', + closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) # enable asserts with_env_var('SOLARGRAPH_ASSERTS', 'on') do @@ -13,8 +15,10 @@ end it "will not combine pins with different directives" do - pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro') - pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro other') + pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', + closure: Solargraph::Pin::ROOT_PIN) + pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro other', + closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) with_env_var('SOLARGRAPH_ASSERTS', 'on') do expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) From f97cad52d10b85b4798240fa5d1a28902114fcfd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 21 May 2025 15:42:16 -0400 Subject: [PATCH 030/116] GemPins pin merging improvements --- lib/solargraph/pin/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index e7acf20f1..d9f0d5223 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -200,7 +200,6 @@ def choose_pin_attr_with_same_name(other, attr) if val1&.name != val2&.name Solargraph.assert_or_log(:combine_with, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") - return nil end [val1, val2].min end From 70d91069e3dea3abba853c69e53f1ad7bfa0c128 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 07:20:25 -0400 Subject: [PATCH 031/116] GemPins pin merging improvements --- lib/solargraph/pin/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index d9f0d5223..3a55d4c99 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -201,7 +201,7 @@ def choose_pin_attr_with_same_name(other, attr) Solargraph.assert_or_log(:combine_with, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end - [val1, val2].min + [val1, val2].compact.min end # @param other [self] From 2e1b0eb3c430bbb09f5dae4a0d03e14b26c64dbd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 07:53:54 -0400 Subject: [PATCH 032/116] Support ActiveSupport::Concern pattern for class methods --- .yardopts | 1 + lib/solargraph/api_map.rb | 20 ++++++++++++++++++++ lib/solargraph/pin/method.rb | 8 +++++++- lib/solargraph/yardoc.rb | 2 +- solargraph.gemspec | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) 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/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 9443e8529..b74de2b93 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -643,6 +643,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 @@ -664,6 +666,24 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false result.concat inner_get_methods(fqsc, scope, visibility, true, skip, no_core) unless fqsc.nil? end else + store.get_includes(fqns).reverse.each do |include_tag| + logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + rooted_include_tag = qualify(include_tag, rooted_tag) + + # ActiveSupport::Concern is syntactic sugar for a common + # pattern to provide virtual class method - i.e., if Foo + # includes Bar and Bar is a module using this + # pattern, Bar can supply class methods which will also + # appear under Foo. + + # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html + included_class_pins = inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) + # activesupport_concern_pins = included_class_pins.select { |p| p.virtual_class_method? } + # result.concat activesupport_concern_pins + result.concat included_class_pins # TODO remove this line once we have activesupport::concern support + 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? diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 5c94647e6..cb0aa3704 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -10,6 +10,10 @@ class Method < Callable # @return [::Symbol] :public, :private, or :protected attr_reader :visibility + def virtual_class_method? + @virtual_class_method + end + # @return [Parser::AST::Node] attr_reader :node @@ -20,7 +24,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, + virtual_class_method: false, **splat super(**splat) @visibility = visibility @explicit = explicit @@ -29,6 +34,7 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @attribute = attribute @signatures = signatures @anon_splat = anon_splat + @virtual_class_method = virtual_class_method end def == other diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 4fd9b193f..858c7774a 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 2bfb6dbdf..b19772703 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -41,6 +41,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' From 4e6692830e241270e9bfe45d13074162986e0555 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 09:09:33 -0400 Subject: [PATCH 033/116] Improve parameter setting --- lib/solargraph/pin/callable.rb | 2 +- lib/solargraph/pin/method.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 02f28ae84..68ee2ce5f 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -44,7 +44,7 @@ def combine_with(other, attrs={}) block: combine_blocks(other), return_type: combine_return_type(other), }.merge(attrs) - new_attrs[:parameters] = choose_parameters(other) unless new_attrs.key?(:parameters) + new_attrs[:parameters] = choose_parameters(other).clone.freeze unless new_attrs.key?(:parameters) super(other, new_attrs) end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 3dc2fe92f..752d9b702 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -57,14 +57,19 @@ def combine_visibility(other) end def combine_with(other, attrs = {}) - sigs = combine_all_signature_pins(*(self.signatures + other.signatures)) + sigs = combine_all_signature_pins(*(self.signatures + other.signatures)).clone.freeze + parameters = if sigs.length > 0 + [].freeze + else + choose(other, :parameters).clone.freeze + end new_attrs = { visibility: combine_visibility(other), explicit: explicit? || other.explicit?, block: combine_blocks(other), node: choose_node(other, :node), attribute: prefer_rbs_location(other, :attribute?), - parameters: choose(other, :parameters), + parameters: parameters, signatures: sigs, anon_splat: assert_same(other, :anon_splat?), return_type: nil # pulled from signatures on first call From 56f4699b821861dc79625e7c696f38e8d120e99f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 12:50:42 -0400 Subject: [PATCH 034/116] GemPins pin merging improvements --- lib/solargraph/pin/parameter.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 8fa1da7df..db0cdc113 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -38,9 +38,8 @@ def needs_consistent_name? keyword? end - def arity_decl - name = (name || '(anon)') + name = (self.name || '(anon)') type = (return_type&.to_rbs || 'untyped') case decl when :arg From ed054c7e39e7163b277ff76ceb1757bd3d229290 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 14:27:18 -0400 Subject: [PATCH 035/116] GemPins pin merging improvements --- lib/solargraph/pin/method.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 752d9b702..1a5af98fe 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -422,7 +422,13 @@ def dodgy_visibility_source? # as of 2025-03-12, the RBS generator used for # e.g. activesupport did not understand 'private' markings # inside 'class << self' blocks, but YARD did OK at it - source == :rbs && scope == :class && type_location&.filename&.include?('generated') + source == :rbs && scope == :class && type_location&.filename&.include?('generated') || + # private on attr_readers seems to be broken in Prism's auto-generator script + source == :rbs && scope == :instance && namespace.start_with?('Prism::') || + # The RBS for the RBS gem itself seems to use private as a + # 'is this a public API' concept, more aggressively than the + # actual code. Let's respect that and ignore the actual .rb file. + source == :yardoc && scope == :instance && namespace.start_with?('RBS::') end private From 44cbbdbd07f35a19596963589318f8372efe3cb7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 14:56:15 -0400 Subject: [PATCH 036/116] Support class-scoped aliases and attributes from RBS --- lib/solargraph/pin/base.rb | 3 ++- lib/solargraph/rbs_map/conversions.rb | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index b81b28df3..a3ee4e71a 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -36,12 +36,13 @@ def presence_certain? # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] # @param comments [String] - def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '' + def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '', source: nil @location = location @type_location = type_location @closure = closure @name = name @comments = comments + @source = source end # @return [String] diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index db564e39d..7800e9036 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -391,13 +391,15 @@ def parts_of_function type, pin # @param closure [Pin::Namespace] # @return [void] def attr_reader_to_pin(decl, closure) + final_scope = decl.kind == :instance ? :instance : :class pin = Solargraph::Pin::Method.new( name: decl.name.to_s, type_location: location_decl_to_pin_location(decl.location), closure: closure, comments: decl.comment&.string, - scope: :instance, - attribute: true + scope: final_scope, + attribute: true, + source: :rbs, ) 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)) @@ -408,13 +410,15 @@ def attr_reader_to_pin(decl, closure) # @param closure [Pin::Namespace] # @return [void] def attr_writer_to_pin(decl, closure) + final_scope = decl.kind == :instance ? :instance : :class pin = Solargraph::Pin::Method.new( name: "#{decl.name.to_s}=", type_location: location_decl_to_pin_location(decl.location), closure: closure, comments: decl.comment&.string, - scope: :instance, - attribute: true + scope: final_scope, + attribute: true, + source: :rbs ) 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)) @@ -514,11 +518,14 @@ def extend_to_pin decl, closure # @param closure [Pin::Namespace] # @return [void] def alias_to_pin decl, closure + final_scope = decl.singleton? ? :class : :instance pins.push Solargraph::Pin::MethodAlias.new( name: decl.new_name.to_s, type_location: location_decl_to_pin_location(decl.location), original: decl.old_name.to_s, - closure: closure + closure: closure, + scope: final_scope, + source: :rbs, ) end From 998dc61112fe6caac7c5da9653897361ec303d95 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 15:31:46 -0400 Subject: [PATCH 037/116] Improved handling of YARD macros --- lib/solargraph/pin/base.rb | 34 ++++++++++++++++++++++++++++++++-- spec/pin/base_spec.rb | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 3a55d4c99..49d80359f 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -38,6 +38,7 @@ def presence_certain? # @param closure [Solargraph::Pin::Closure, nil] # @param name [String] # @param comments [String] + # @param source [Symbol, nil] def initialize location: nil, type_location: nil, closure: nil, name: '', comments: '', docstring: nil, source: nil, directives: nil @location = location @type_location = type_location @@ -69,7 +70,7 @@ def combine_with(other, attrs={}) docstring: choose(other, :docstring), directives: combine_directives(other), }.merge(attrs) - assert_same(other, :macros) + assert_same_macros(other) logger.debug { "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{self.comments}" } out = self.class.new(**new_attrs) out.reset_generated! @@ -181,6 +182,35 @@ def rbs_location? type_location&.rbs? end + def assert_same_macros(other) + assert_same_count(other, :macros) + assert_same_array_content(other, :macros) { |macro| macro.tag.name } + end + + def assert_same_array_content(other, attr, &block) + arr1 = send(attr) + arr2 = other.send(attr) + values1 = arr1.map(&block) + values2 = arr2.map(&block) + return arr1 if values1 == values2 + Solargraph.assert_or_log("combine_with_#{attr}".to_sym, + "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self values = #{values1}\nother values =#{attr} = #{values2}") + arr1 + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [Object, nil] + def assert_same_count(other, attr) + val1 = send(attr) + val2 = other.send(attr) + return val1 if val1.count == val2.count + Solargraph.assert_or_log("combine_with_#{attr}".to_sym, + "Inconsistent #{attr.inspect} count value between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") + val1 + end + # @param other [self] # @param attr [::Symbol] # @@ -189,7 +219,7 @@ def assert_same(other, attr) val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 - Solargraph.assert_or_log(:combine_with, + Solargraph.assert_or_log("combine_with_#{attr}".to_sym, "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") val1 end diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index a66dc52ac..64e5b808c 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -10,7 +10,7 @@ expect(pin1.nearly?(pin2)).to be(false) # enable asserts with_env_var('SOLARGRAPH_ASSERTS', 'on') do - expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) + expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros count/) end end From 4bc4b257b7a5ee53ecf1ecd716026d329b932735 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 26 May 2025 11:07:13 -0400 Subject: [PATCH 038/116] Tuple enabler: infer literal types and use them for signature selection (#836) * Infer literal types and use them for signature selection * Hold back on representing literal strings * Resolve merge * Update spec/source_map/clip_spec.rb * Resolve merge issues * Update test expectations * Update test expectations * Fix merge issue * Add regression test around assignment in return position * Adjust spec expectations * Fix merge * Resolve merge issue * Render string literals from RBS in a useful way * Treat literal nil as NilClass for method resolution and vice versa Apply same for true/TrueClass and false/FalseClass * Annotate hash literal * Fix merge issue --- lib/solargraph/api_map.rb | 10 +- lib/solargraph/api_map/store.rb | 2 + lib/solargraph/complex_type.rb | 33 ++- lib/solargraph/complex_type/unique_type.rb | 64 ++++- .../parser/parser_gem/node_chainer.rb | 6 +- .../parser/parser_gem/node_methods.rb | 2 + lib/solargraph/pin/base_variable.rb | 4 + lib/solargraph/pin/parameter.rb | 9 + lib/solargraph/rbs_map/conversions.rb | 2 +- lib/solargraph/source/chain.rb | 2 +- lib/solargraph/source/chain/array.rb | 7 +- lib/solargraph/source/chain/call.rb | 11 +- lib/solargraph/source/chain/hash.rb | 5 +- lib/solargraph/source/chain/literal.rb | 24 +- lib/solargraph/source/source_chainer.rb | 4 +- lib/solargraph/type_checker/checks.rb | 4 + spec/api_map_spec.rb | 15 ++ spec/complex_type_spec.rb | 74 +++++- spec/pin/base_variable_spec.rb | 5 +- spec/pin/method_spec.rb | 14 +- spec/pin/parameter_spec.rb | 12 +- spec/rbs_map/core_map_spec.rb | 22 ++ spec/source/chain/array_spec.rb | 2 +- spec/source/chain/call_spec.rb | 6 +- spec/source/chain/literal_spec.rb | 5 +- spec/source/chain_spec.rb | 20 +- spec/source_map/clip_spec.rb | 251 ++++++++++++++++-- spec/type_checker/levels/strict_spec.rb | 46 ++++ spec/type_checker/levels/typed_spec.rb | 18 ++ 29 files changed, 592 insertions(+), 87 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 5cd549124..d71b7ca18 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -271,15 +271,19 @@ def get_namespace_pins namespace, context # Should not be prefixed with '::'. # @return [String, nil] fully qualified tag def qualify tag, context_tag = '' - return tag if ['self', nil].include?(tag) + return tag if ['Boolean', 'self', nil].include?(tag) - context_type = ComplexType.parse(context_tag).force_rooted + context_type = ComplexType.try_parse(context_tag).force_rooted return unless context_type type = ComplexType.try_parse(tag) return unless type + return tag if type.literal? - fqns = qualify_namespace(type.rooted_namespace, context_type.namespace) + context_type = ComplexType.try_parse(context_tag) + return unless context_type + + fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace) return unless fqns fqns + type.substring diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index b03044fc4..31f1d8281 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -73,6 +73,8 @@ def get_superclass fqns return superclass_references[fqns].first if superclass_references.key?(fqns) return 'Object' if fqns != 'BasicObject' && namespace_exists?(fqns) return 'Object' if fqns == 'Boolean' + simplified_literal_name = ComplexType.parse("#{fqns}").simplify_literals.name + return simplified_literal_name if simplified_literal_name != fqns nil end diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index add53e83d..53e52693c 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -73,7 +73,7 @@ def self_to_type dst end # @yieldparam [UniqueType] - # @return [Array] + # @return [Array] def map &block @items.map &block end @@ -96,6 +96,12 @@ def each_unique_type &block end end + # @param atype [ComplexType] type which may be assigned to this type + # @param api_map [ApiMap] The ApiMap that performs qualification + def can_assign?(api_map, atype) + any? { |ut| ut.can_assign?(api_map, atype) } + end + # @return [Integer] def length @items.length @@ -106,10 +112,6 @@ def to_a @items end - def tags - @items.map(&:tag).join(', ') - end - # @param index [Integer] # @return [UniqueType] def [](index) @@ -150,6 +152,23 @@ def to_s map(&:tag).join(', ') end + def tags + map(&:tag).join(', ') + end + + def simple_tags + simplify_literals.tags + end + + def literal? + @items.any?(&:literal?) + end + + # @return [ComplexType] + def downcast_to_literal_if_possible + ComplexType.new(items.map(&:downcast_to_literal_if_possible)) + end + def desc rooted_tags end @@ -178,6 +197,10 @@ def generic? any?(&:generic?) end + def simplify_literals + ComplexType.new(map(&:simplify_literals)) + end + # @param new_name [String, nil] # @yieldparam t [UniqueType] # @yieldreturn [UniqueType] diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 1771ee911..138a637a5 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -27,7 +27,7 @@ class UniqueType # @return [UniqueType] def self.parse name, substring = '', make_rooted: nil if name.start_with?(':::') - raise "Illegal prefix: #{name}" + raise ComplexTypeError, "Illegal prefix: #{name}" end if name.start_with?('::') name = name[2..-1] @@ -48,7 +48,7 @@ def self.parse name, substring = '', make_rooted: nil subs = ComplexType.parse(substring[1..-2], partial: true) parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0]) if parameters_type == :hash - raise ComplexTypeError, "Bad hash type" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType) + raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring}" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType) # @todo should be able to resolve map; both types have it # with same return type # @sg-ignore @@ -86,6 +86,38 @@ def to_s tag end + def simplify_literals + transform do |t| + next t unless t.literal? + t.recreate(new_name: t.non_literal_name) + end + end + + def literal? + non_literal_name != name + end + + def non_literal_name + @non_literal_name ||= determine_non_literal_name + end + + def determine_non_literal_name + # https://github.com/ruby/rbs/blob/master/docs/syntax.md + # + # _literal_ ::= _string-literal_ + # | _symbol-literal_ + # | _integer-literal_ + # | `true` + # | `false` + return name if name.empty? + return 'NilClass' if name == 'nil' + return 'Boolean' if ['true', 'false'].include?(name) + return 'Symbol' if name[0] == ':' + return 'String' if ['"', "'"].include?(name[0]) + return 'Integer' if name.match?(/^-?\d+$/) + name + end + def eql?(other) self.class == other.class && @name == other.name && @@ -113,6 +145,8 @@ def items def rbs_name if name == 'undefined' 'untyped' + elsif literal? + name else rooted_name end @@ -183,6 +217,23 @@ def generic? name == GENERIC_TAG_NAME || all_params.any?(&:generic?) end + # @param api_map [ApiMap] The ApiMap that performs qualification + # @param atype [ComplexType] type which may be assigned to this type + def can_assign?(api_map, atype) + logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" } + downcasted_atype = atype.downcast_to_literal_if_possible + out = downcasted_atype.all? do |autype| + autype.name == name || api_map.super_and_sub?(name, autype.name) + end + logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" } + out + end + + # @return [UniqueType] + def downcast_to_literal_if_possible + SINGLE_SUBTYPE.fetch(rooted_tag, self) + end + # @param generics_to_resolve [Enumerable] # @param context_type [UniqueType, nil] # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved @@ -385,6 +436,15 @@ def self.can_root_name?(name) UNDEFINED = UniqueType.new('undefined', rooted: false) BOOLEAN = UniqueType.new('Boolean', rooted: true) + TRUE = UniqueType.new('true', rooted: true) + FALSE = UniqueType.new('false', rooted: true) + NIL = UniqueType.new('nil', rooted: true) + # @type [Hash{String => UniqueType}] + SINGLE_SUBTYPE = { + '::TrueClass' => UniqueType::TRUE, + '::FalseClass' => UniqueType::FALSE, + '::NilClass' => UniqueType::NIL + }.freeze include Logging end diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index 2cce95c4b..355deeb13 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -129,13 +129,13 @@ def generate_links n end end elsif n.type == :hash - result.push Chain::Hash.new('::Hash', hash_is_splatted?(n)) + result.push Chain::Hash.new('::Hash', n, hash_is_splatted?(n)) elsif n.type == :array chained_children = n.children.map { |c| NodeChainer.chain(c) } - result.push Source::Chain::Array.new(chained_children) + result.push Source::Chain::Array.new(chained_children, n) else lit = infer_literal_node_type(n) - result.push (lit ? Chain::Literal.new(lit) : Chain::Link.new) + result.push (lit ? Chain::Literal.new(lit, n) : Chain::Link.new) end result end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 7101b6f5c..9b1e13f31 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -59,6 +59,8 @@ def infer_literal_node_type node return '::String' elsif node.type == :array return '::Array' + elsif node.type == :nil + return '::NilClass' elsif node.type == :hash return '::Hash' elsif node.type == :int diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index e333a2858..8f0644cae 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -34,7 +34,10 @@ def return_type @return_type ||= generate_complex_type end + # @sg-ignore def nil_assignment? + # this will always be false - should it be return_type == + # ComplexType::NIL or somesuch? return_type.nil? end @@ -91,6 +94,7 @@ def probe api_map ComplexType::UNDEFINED end + # @param other [Object] def == other return false unless super assignment == other.assignment diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 047026588..0f1deab79 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -126,6 +126,15 @@ def typify api_map 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 + def documentation tag = param_tag return '' if tag.nil? || tag.text.nil? diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index db564e39d..d1265a434 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -575,7 +575,7 @@ def other_type_to_tag type elsif type.is_a?(RBS::Types::Tuple) "Array(#{type.types.map { |t| other_type_to_tag(t) }.join(', ')})" elsif type.is_a?(RBS::Types::Literal) - type.literal.to_s + type.literal.inspect elsif type.is_a?(RBS::Types::Union) type.types.map { |t| other_type_to_tag(t) }.join(', ') elsif type.is_a?(RBS::Types::Record) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index af7669870..90935f866 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -130,7 +130,7 @@ def infer api_map, name_pin, locals @@inference_invalidation_key = api_map.hash @@inference_cache = {} end - out = infer_uncached api_map, name_pin, locals + out = infer_uncached(api_map, name_pin, locals).downcast_to_literal_if_possible logger.debug { "Chain#infer() - caching result - cache_key_hash=#{cache_key.hash}, links.map(&:hash)=#{links.map(&:hash)}, links=#{links}, cache_key.map(&:hash) = #{cache_key.map(&:hash)}, cache_key=#{cache_key}" } @@inference_cache[cache_key] = out end diff --git a/lib/solargraph/source/chain/array.rb b/lib/solargraph/source/chain/array.rb index 26c584e51..db37f1856 100644 --- a/lib/solargraph/source/chain/array.rb +++ b/lib/solargraph/source/chain/array.rb @@ -3,8 +3,9 @@ class Source class Chain class Array < Literal # @param children [::Array] - def initialize children - super('::Array') + # @param node [Parser::AST::Node] + def initialize children, node + super('::Array', node) @children = children end @@ -17,7 +18,7 @@ def word # @param locals [::Array] def resolve api_map, name_pin, locals child_types = @children.map do |child| - child.infer(api_map, name_pin, locals) + child.infer(api_map, name_pin, locals).simplify_literals end type = if child_types.uniq.length == 1 && child_types.first.defined? diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index e0220d719..ccc245301 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -101,7 +101,8 @@ def inferred_pins pins, api_map, name_pin, locals # use it. If we didn't pass a block, the logic below will # reject it regardless - sorted_overloads = overloads.sort { |ol| ol.block? ? -1 : 1 } + with_block, without_block = overloads.partition(&:block?) + sorted_overloads = with_block + without_block new_signature_pin = nil atypes = [] sorted_overloads.each do |ol| @@ -124,13 +125,7 @@ def inferred_pins pins, api_map, name_pin, locals 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), locals) - # make sure we get types from up the method - # inheritance chain if we don't have them on this pin - ptype = param.typify api_map - # @todo Weak type comparison - # unless atype.tag == param.return_type.tag || api_map.super_and_sub?(param.return_type.tag, atype.tag) - unless ptype.undefined? || atype.name == ptype.name || ptype.any? { |current_ptype| api_map.super_and_sub?(current_ptype.name, atype.name) } || ptype.generic? || param.restarg? - logger.debug { "Call#inferred_pins(name_pin.binder=#{name_pin.binder}, name_pin.context=#{name_pin.context}, word=#{word}, atypes=#{atypes.map(&:rooted_tags)}, name_pin=#{name_pin}) - rejecting signature #{ol}" } + unless param.compatible_arg?(atype, api_map) || param.restarg? match = false break end diff --git a/lib/solargraph/source/chain/hash.rb b/lib/solargraph/source/chain/hash.rb index 33b88e7b7..79b63cb3d 100644 --- a/lib/solargraph/source/chain/hash.rb +++ b/lib/solargraph/source/chain/hash.rb @@ -5,9 +5,10 @@ class Source class Chain class Hash < Literal # @param type [String] + # @param node [Parser::AST::Node] # @param splatted [Boolean] - def initialize type, splatted = false - super(type) + def initialize type, node, splatted = false + super(type, node) @splatted = splatted end diff --git a/lib/solargraph/source/chain/literal.rb b/lib/solargraph/source/chain/literal.rb index ff9b93819..471ca2cef 100644 --- a/lib/solargraph/source/chain/literal.rb +++ b/lib/solargraph/source/chain/literal.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'parser' + module Solargraph class Source class Chain @@ -8,9 +10,22 @@ def word @word ||= "<#{@type}>" end + attr_reader :value + # @param type [String] - def initialize type + # @param node [Parser::AST::Node, Object] + def initialize type, node + if node.is_a?(::Parser::AST::Node) + if node.type == :true + @value = true + elsif node.type == :false + @value = false + elsif [:int, :sym].include?(node.type) + @value = node.children.first + end + end @type = type + @literal_type = ComplexType.try_parse(@value.inspect) @complex_type = ComplexType.try_parse(type) end @@ -20,7 +35,12 @@ def initialize type end def resolve api_map, name_pin, locals - [Pin::ProxyType.anonymous(@complex_type)] + if api_map.super_and_sub?(@complex_type.name, @literal_type.name) + [Pin::ProxyType.anonymous(@literal_type)] + else + # we don't support this value as a literal type + [Pin::ProxyType.anonymous(@complex_type)] + end end end end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index 8c6605fb1..e79d85b7e 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -32,8 +32,8 @@ 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'), Chain::UNDEFINED_CALL]) if phrase =~ /^[0-9]+\.$/ - return Chain.new([Chain::Literal.new('Symbol')]) if phrase.start_with?(':') && !phrase.start_with?('::') + 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?('..') diff --git a/lib/solargraph/type_checker/checks.rb b/lib/solargraph/type_checker/checks.rb index f0a7febcf..de402978b 100644 --- a/lib/solargraph/type_checker/checks.rb +++ b/lib/solargraph/type_checker/checks.rb @@ -50,6 +50,8 @@ def types_match? api_map, expected, inferred # @param inferred [ComplexType] # @return [Boolean] def any_types_match? api_map, expected, inferred + expected = expected.downcast_to_literal_if_possible + inferred = inferred.downcast_to_literal_if_possible return duck_types_match?(api_map, expected, inferred) if expected.duck_type? # walk through the union expected type and see if any members # of the union match the inferred type @@ -71,6 +73,8 @@ def any_types_match? api_map, expected, inferred # @param expected [ComplexType] # @return [Boolean] def all_types_match? api_map, inferred, expected + expected = expected.downcast_to_literal_if_possible + inferred = inferred.downcast_to_literal_if_possible return duck_types_match?(api_map, expected, inferred) if expected.duck_type? inferred.each do |inf| next if inf.duck_type? diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 85e373341..e3e0b19f4 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -744,6 +744,21 @@ def bar; end expect(pins.map(&:name).sort).to eq(%w[bar foo]) end + it 'can qualify "Boolean"' do + api_map = Solargraph::ApiMap.new + expect(api_map.qualify('Boolean')).to eq('Boolean') + end + + it 'knows that true is a "subtype" of Boolean' do + api_map = Solargraph::ApiMap.new + expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) + end + + it 'knows that false is a "subtype" of Boolean' do + api_map = Solargraph::ApiMap.new + expect(api_map.super_and_sub?('Boolean', 'true')).to be(true) + end + it 'resolves aliases for YARD methods' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) yard_pins = Dir.chdir dir do diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 0d0097c56..fa840279d 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -387,7 +387,6 @@ def make_bar end it 'resolves generic namespace parameters' do - api_map = Solargraph::ApiMap.new return_type = Solargraph::ComplexType.parse('Array>') generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: '@generic GenericTypeParam') called_method = Solargraph::Pin::Method.new( @@ -401,7 +400,6 @@ def make_bar end it 'resolves generic parameters on a tuple using ()' do - api_map = Solargraph::ApiMap.new return_type = Solargraph::ComplexType.parse('Array(generic, generic)') generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") called_method = Solargraph::Pin::Method.new( @@ -415,7 +413,6 @@ def make_bar end it 'resolves generic parameters on a tuple using <()>' do - api_map = Solargraph::ApiMap.new return_type = Solargraph::ComplexType.parse('Array<(generic, generic)>') generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") called_method = Solargraph::Pin::Method.new( @@ -428,17 +425,63 @@ def make_bar expect(type.tag).to eq('Array<(String, Integer)>') end + # See literal details at + # https://github.com/ruby/rbs/blob/master/docs/syntax.md and + # https://yardoc.org/types.html + xit 'understands literal strings with double quotes' do + type = Solargraph::ComplexType.parse('"foo"') + expect(type.tag).to eq('"foo"') + expect(type.to_rbs).to eq('"foo"') + expect(type.to_s).to eq('String') + end + + xit 'understands literal strings with single quotes' do + type = Solargraph::ComplexType.parse("'foo'") + expect(type.tag).to eq("'foo'") + expect(type.to_rbs).to eq("'foo'") + expect(type.to_s).to eq('String') + end + + it 'understands literal symbols' do + type = Solargraph::ComplexType.parse(':foo') + expect(type.tag).to eq(':foo') + expect(type.to_rbs).to eq(':foo') + expect(type.to_s).to eq(':foo') + end + + it 'understands literal integers' do + type = Solargraph::ComplexType.parse('123') + expect(type.tag).to eq('123') + expect(type.to_rbs).to eq('123') + expect(type.to_s).to eq('123') + end + + it 'understands literal true' do + type = Solargraph::ComplexType.parse('true') + expect(type.tag).to eq('true') + expect(type.to_rbs).to eq('true') + expect(type.to_s).to eq('true') + end + + it 'understands literal false' do + type = Solargraph::ComplexType.parse('false') + expect(type.tag).to eq('false') + expect(type.to_rbs).to eq('false') + expect(type.to_s).to eq('false') + end + it 'parses tuples of tuples' do - api_map = Solargraph::ApiMap.new type = Solargraph::ComplexType.parse('Array(Array(String), String)') expect(type.tag).to eq('Array(Array(String), String)') + expect(type.to_rbs).to eq('[[String], String]') + expect(type.to_s).to eq('Array(Array(String), String)') end it 'parses tuples of tuples with same type twice in a row' do - api_map = Solargraph::ApiMap.new type = Solargraph::ComplexType.parse('Array(Symbol, String, Array(Integer, Integer))') - expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') + expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') + expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') end it 'qualifies tuples of tuples with same type twice in a row' do @@ -449,6 +492,25 @@ def make_bar expect(type.to_rbs).to eq('[::Symbol, ::String, [::Integer, ::Integer]]') end + it 'squashes literal types when simplifying literals of same type' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('1, 2, 3') + type = type.qualify(api_map) + expect(type.to_s).to eq('1, 2, 3') + expect(type.tags).to eq('1, 2, 3') + expect(type.simple_tags).to eq('Integer') + expect(type.to_rbs).to eq('(1 | 2 | 3)') + end + + xit 'stops parsing when the first character indicates a string literal' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('"Array(Symbol, String, Array(Integer, Integer)"') + type = type.qualify(api_map) + expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') + expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') + expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') + end + ['generic', "nil", "true", "false", ":123", "123"].each do |tag| it "treats #{tag} as rooted" do types = Solargraph::ComplexType.parse(tag) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 349105957..8c462bff3 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -39,6 +39,9 @@ def bar api_map.map source pin = api_map.get_instance_variable_pins('Foo').first type = pin.probe(api_map) - expect(type.to_s).to eq('Integer, nil') + expect(type.tags).to eq('1, nil') + expect(type.simple_tags).to eq('Integer, NilClass') + expect(type.to_rbs).to eq('(1 | nil)') + expect(type.simplify_literals.to_rbs).to eq('(::Integer | ::NilClass)') end end diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index a8afda31e..30136d889 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -222,7 +222,7 @@ def bar(a) api_map.map source pin = api_map.get_path_pins('Foo#bar').first type = pin.probe(api_map) - expect(type.tag).to eq('Integer') + expect(type.simple_tags).to eq('Integer') end it 'infers return types from other parameters' do @@ -323,7 +323,9 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first type = pin.probe(api_map) - expect(type.to_s).to eq('Integer, nil') + expect(type.rooted_tags).to eq('1, nil') + expect(type.to_rbs).to eq('(1 | nil)') + expect(type.simple_tags).to eq('Integer, NilClass') end it 'infers from chains' do @@ -354,7 +356,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first type = pin.probe(api_map) - expect(type.to_s).to eq('Integer') + expect(type.simple_tags).to eq('Integer') end it 'infers from literal array dereference' do @@ -582,7 +584,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first expect(pin.typify(api_map)).to be_undefined - expect(pin.probe(api_map).items.map(&:tag)).to eq(%w[String Integer]) + expect(pin.probe(api_map).simple_tags).to eq('String, Integer') end it 'infers return types from begin rescue block' do @@ -601,7 +603,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first expect(pin.typify(api_map)).to be_undefined - expect(pin.probe(api_map).items.map(&:tag)).to eq(%w[String Integer]) + expect(pin.probe(api_map).simple_tags).to eq('String, Integer') end it 'infers return types from compound statements in conditionals' do @@ -617,7 +619,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first expect(pin.typify(api_map)).to be_undefined - expect(pin.probe(api_map).items.map(&:tag)).to eq(%w[Symbol Float String Integer]) + expect(pin.probe(api_map).simple_tags).to eq('Symbol, Float, String, Integer') end it 'ignores malformed overload tags' do diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 9010d1524..5563aed34 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -389,7 +389,7 @@ def foo bar = 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers types from kwoptarg values' do @@ -403,7 +403,7 @@ def foo bar: 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end end @@ -419,7 +419,7 @@ def self.foo bar = 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers types from kwoptarg values' do @@ -433,7 +433,7 @@ def self.foo bar: 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers types from kwoptarg code' do @@ -465,7 +465,7 @@ def self.foo bar = 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers types from kwoptarg values' do @@ -481,7 +481,7 @@ def self.foo bar: 'bar' api_map.map(source) pin = api_map.source_map('test.rb').locals.first type = pin.probe(api_map) - expect(type.name).to eq('String') + expect(type.simple_tags).to eq('String') end end end diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index ec9f85ca1..5c0d2f870 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -103,4 +103,26 @@ class Foo end end end + + it 'renders string literals from RBS in a useful way' do + source = Solargraph::Source.load_string(%( + foo = nil + bar = foo.to_s # => '""' in rbs + bar + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [3, 6]) + expect(clip.infer.to_s).to eq('""') + expect(clip.infer.to_rbs).to eq('""') + end + + it 'treats literal nil as NilClass for method resolution' do + source = Solargraph::Source.load_string(%( + foo = nil.to_s # => "''" in rbs + foo + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [2, 6]) + expect(clip.infer.to_s).to eq('""') + end end diff --git a/spec/source/chain/array_spec.rb b/spec/source/chain/array_spec.rb index f483c6253..b8ef9db23 100644 --- a/spec/source/chain/array_spec.rb +++ b/spec/source/chain/array_spec.rb @@ -1,6 +1,6 @@ describe Solargraph::Source::Chain::Array do it "resolves an instance of an array" do - literal = described_class.new([]) + literal = described_class.new([], nil) pin = literal.resolve(nil, nil, nil).first expect(pin.return_type.tag).to eq('Array') end diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 9f5e1f5a0..44665eaa7 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -137,7 +137,7 @@ def yielder(&blk) api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(7, 8)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) - expect(type.tag).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers generic parameterized types through module inclusion' do @@ -247,7 +247,7 @@ def baz api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(9, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) - expect(type.tag).to eq('Integer') + expect(type.simple_tags).to eq('Integer') end xit 'infers method return types based on method generic' do @@ -286,7 +286,7 @@ def baz(&block) api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(9, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) - expect(type.tag).to eq('Integer') + expect(type.simple_tags).to eq('Integer') end it 'infers generic types' do diff --git a/spec/source/chain/literal_spec.rb b/spec/source/chain/literal_spec.rb index c87c1dc23..a1431ae07 100644 --- a/spec/source/chain/literal_spec.rb +++ b/spec/source/chain/literal_spec.rb @@ -1,7 +1,8 @@ describe Solargraph::Source::Chain::Literal do it "resolves an instance of a literal" do - literal = described_class.new('String') - pin = literal.resolve(nil, nil, nil).first + literal = described_class.new('String', nil) + api_map = Solargraph::ApiMap.new + pin = literal.resolve(api_map, nil, nil).first expect(pin.return_type.tag).to eq('String') end end diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index 6f851c15d..abc8c2b05 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -44,7 +44,7 @@ end it "recognizes literals" do - chain = described_class.new([Solargraph::Source::Chain::Literal.new('String')]) + chain = described_class.new([Solargraph::Source::Chain::Literal.new('String', nil)]) expect(chain.literal?).to be(true) end @@ -149,7 +149,7 @@ module Other api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(2, 11)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.to_s).to eq('String') + expect(type.simple_tags).to eq('String') end it "uses last line of a begin expression as return type" do @@ -163,7 +163,7 @@ module Other api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.to_s).to eq('String') + expect(type.simple_tags).to eq('String') end it "matches constants on complete symbols" do @@ -197,7 +197,7 @@ class NotCorrect; end api_map = Solargraph::ApiMap.new chain = Solargraph::Parser.chain(source.node) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.to_s).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers multiple types from or-nodes' do @@ -207,7 +207,7 @@ class NotCorrect; end api_map = Solargraph::ApiMap.new chain = Solargraph::Parser.chain(source.node) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.to_s).to eq('Array, String') + expect(type.simple_tags).to eq('Array, String') end it 'infers Procs from block-pass nodes' do @@ -235,7 +235,7 @@ class NotCorrect; end # chain = Solargraph::Source::NodeChainer.chain(node, 'test.rb') chain = Solargraph::Parser.chain(node, 'test.rb') type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.tag).to eq('Boolean') + expect(type.tag).to eq('true') end it 'infers self from Object#freeze' do @@ -313,7 +313,7 @@ def self.core_string api_map.map source chain = Solargraph::Parser.chain(node) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.tag).to eq('Symbol') + expect(type.simple_tags).to eq('Symbol') end it 'infers Symbol from quoted symbols' do @@ -323,7 +323,7 @@ def self.core_string api_map.map source chain = Solargraph::Parser.chain(node) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) - expect(type.tag).to eq('Symbol') + expect(type.simple_tags).to eq('Symbol') end it 'infers Symbol from interpolated symbols' do @@ -362,7 +362,7 @@ class Bar; end expect(chain.links[1]).to be_with_block end - it 'infers instance variables from multiple assignments' do + xit 'infers instance variables from multiple assignments' do source = Solargraph::Source.load_string(%( def foo @foo = nil @@ -373,7 +373,7 @@ def foo api_map.map source pin = api_map.get_path_pins('#foo').first type = pin.probe(api_map) - expect(type.tag).to eq('String') + expect(type.simple_tags).to eq('String') end it 'recognizes nil safe navigation' do diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 65af90cb2..7c035dd8d 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -317,7 +317,7 @@ def foo map.map source clip = map.clip_at('test.rb', Solargraph::Position.new(8, 6)) type = clip.infer - expect(type.to_s).to eq('String, Integer') + expect(type.simple_tags).to eq('String, Integer') end xit 'uses flow-sensitive typing to infer non-nil method return type' do @@ -383,7 +383,8 @@ def foo map.map source clip = map.clip_at('test.rb', Solargraph::Position.new(6, 6)) type = clip.infer - expect(type.tag).to eq('Integer') + expect(type.tags).to eq('1') + expect(type.simple_tags).to eq('Integer') end it 'infers return types from instance variables' do @@ -416,7 +417,9 @@ def self.bar map.map source clip = map.clip_at('test.rb', Solargraph::Position.new(6, 10)) type = clip.infer - expect(type.tag).to eq('String') + # @todo expect(type.tags).to eq('"bar"') + expect(type.tags).to eq('String') + expect(type.simple_tags).to eq('String') end it 'infers nil for empty methods' do @@ -643,7 +646,9 @@ def initialize api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [7, 8]) - expect(clip.infer.tag).to eq('String') + # @todo expect(clip.infer.tags).to eq('""') + expect(clip.infer.tags).to eq('String') + expect(clip.infer.simple_tags).to eq("String") end it 'completes instance variable methods in rebound blocks' do @@ -703,7 +708,9 @@ class Foo api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [5, 8]) - expect(clip.infer.tag).to eq('String') + # @todo expect(clip.infer.tags).to eq('""') + expect(clip.infer.tags).to eq('String') + expect(clip.infer.simple_tags).to eq('String') end it 'completes extended class methods' do @@ -786,7 +793,9 @@ class Foo api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [6, 7]) - expect(clip.infer.to_s).to eq('String, Array') + # @todo expect(clip.infer.tags).to eq('"one", Array') + expect(clip.infer.tags).to eq('String, Array') + expect(clip.infer.simple_tags).to eq('String, Array') end it 'detects scoped methods in rebound blocks' do @@ -828,7 +837,9 @@ def my_method api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [15, 20]) - expect(clip.infer.tag).to eq('String') + # @todo expect(clip.infer.tags).to eq('""') + expect(clip.infer.tags).to eq('String') + expect(clip.infer.simple_tags).to eq('String') end it 'finds instance methods inside private classes' do @@ -1653,7 +1664,9 @@ def x api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [9, 7]) type = clip.infer - expect(type.tag).to eq('String') + # @todo expect(type.tags).to eq('"string"') + expect(type.tags).to eq('String') + expect(type.simple_tags).to eq('String') end it 'picks correct overload in Hash#transform_values!' do @@ -1673,12 +1686,18 @@ def bar(t) it 'picks correct overload in Enumerable#max_by' do source = Solargraph::Source.load_string(%( - a = [1, 2, 3].max_by(&:abs) + a = [1, 2, 3] a + b = a.max_by(&:abs) + b ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [2, 6]) type = clip.infer + expect(type.to_s).to eq('Array') + + clip = api_map.clip_at('test.rb', [4, 6]) + type = clip.infer expect(type.to_s).to eq('Integer, nil') end @@ -1882,7 +1901,8 @@ module Foo; class Array; end; end api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [2, 6]) type = clip.infer - expect(type.tag).to eq('Array') + expect(type.tags).to eq('Array<123>') + expect(type.simple_tags).to eq('Array') # @todo more root-safety to be done - expect(type.rooted?).to be true end @@ -1929,7 +1949,8 @@ def foo clip = api_map.clip_at('test.rb', [6, 6]) type = clip.infer - expect(type.tag).to eq('Integer') + expect(type.tags).to eq('123') + expect(type.simple_tags).to eq('Integer') # @todo more root-safety to be done - expect(type.rooted?).to be true end @@ -2040,7 +2061,7 @@ def signatures_at expect(type.to_s).to eq('Integer') end - xit 'identifies tuple types' do + xit 'dereferences tuple types with [](idx) via literals' do source = Solargraph::Source.load_string(%( # @type [Array(String, Integer)] a = 123 @@ -2163,6 +2184,196 @@ def signatures_at expect(clip.infer.to_s).to eq('nil') end + it 'uses types to determine overload to match' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload find(index) + # @param [String] index + # @return [generic] + # @overload find(index) + # @param [Symbol] index + # @return [generic] + def find(index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m.find('foo') + mb + mc = m.find(:bar) + mc +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [16, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [18, 6]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'uses types to determine overload of [] to match' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload [](index) + # @param [String] index + # @return [generic] + # @overload [](index) + # @param [Symbol] index + # @return [generic] + def [](index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m['foo'] + mb + mc = m[:bar] + mc +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [16, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [18, 6]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'uses literal types to determine overload of [] to match' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload [](index) + # @param [1] index + # @return [generic] + # @overload [](index) + # @param [2] index + # @return [generic] + # @overload [](index) + # @param [Integer] index + # @return [Float] + def [](index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m[1] + mb + mc = m[2] + mc + md = m[3] + md +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [19, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [21, 6]) + expect(clip.infer.to_s).to eq('Integer') + + clip = api_map.clip_at('test.rb', [23, 6]) + expect(clip.infer.to_s).to eq('Float') + end + + it 'can use strings and symbols to choose a signature' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload find(index) + # @param [String] index + # @return [generic] + # @overload find(index) + # @param [Symbol] index + # @return [generic] + def find(index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m.find('foo') + mb + mc = m.find(:bar) + mc +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [16, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [18, 6]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'uses types to determine overload of [] to match' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload [](index) + # @param [String] index + # @return [generic] + # @overload [](index) + # @param [Symbol] index + # @return [generic] + def [](index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m['foo'] + mb + mc = m[:bar] + mc +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [16, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [18, 6]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'uses literal types to determine overload of [] to match' do + source = Solargraph::Source.load_string(%( + # @generic A + # @generic B + class Foo + # @overload [](index) + # @param [1] index + # @return [generic] + # @overload [](index) + # @param [2] index + # @return [generic] + # @overload [](index) + # @param [Integer] index + # @return [Float] + def [](index); end + end + + # @type [Foo(String, Integer)] + m = blah + mb = m[1] + mb + mc = m[2] + mc + md = m[3] + md +), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [19, 6]) + expect(clip.infer.to_s).to eq('String') + + clip = api_map.clip_at('test.rb', [21, 6]) + expect(clip.infer.to_s).to eq('Integer') + + clip = api_map.clip_at('test.rb', [23, 6]) + expect(clip.infer.to_s).to eq('Float') + end + it 'interprets self type in superclass method return type' do source = Solargraph::Source.load_string(%( class Foo @@ -2535,7 +2746,7 @@ def bar; end ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 6]) - expect(clip.infer.to_s).to eq('Symbol, Integer, nil') + expect(clip.infer.to_s).to eq(':foo, 123, nil') end it 'expands type with conditional reassignments' do @@ -2551,7 +2762,7 @@ def bar; end api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 6]) # The order of the types can vary between platforms - expect(clip.infer.items.map(&:to_s).sort).to eq(%w[Integer String Symbol]) + expect(clip.infer.items.map(&:to_s).sort).to eq(["123", ":foo", "String"]) end it 'does not map Module methods into an Object' do @@ -2709,16 +2920,16 @@ def baz api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [20, 10]) - expect(clip.infer.to_s).to eq('Array') + expect(clip.infer.to_s).to eq('Array<456>') clip = api_map.clip_at('test.rb', [22, 10]) - expect(clip.infer.to_s).to eq('Array') + expect(clip.infer.to_s).to eq('Array<456>') clip = api_map.clip_at('test.rb', [24, 10]) - expect(clip.infer.to_s).to eq('Array') + expect(clip.infer.to_s).to eq('Array<456>') clip = api_map.clip_at('test.rb', [26, 10]) - expect(clip.infer.to_s).to eq('Array') + expect(clip.infer.to_s).to eq('Array<456>') end xit 'resolves overloads based on kwarg existence' do @@ -2770,9 +2981,9 @@ def foo api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [4, 6]) - expect(clip.infer.to_s).to eq('Array, Hash, Integer, NilClass') + expect(clip.infer.to_s).to eq('Array, Hash, Integer, nil') end - + xit 'infers that type of argument has been overridden' do source = Solargraph::Source.load_string(%( def foo a diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 51c548743..8765e67b6 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -872,5 +872,51 @@ def foo(e) )) expect(checker.problems.map(&:message)).to eq([]) end + + it 'does not complain when passing nil to a NilClass parameter' do + checker = type_checker(%( + # @param a [NilClass] + def foo(a); end + + foo(nil) + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not complain when passing NilClass to nil parameter' do + checker = type_checker(%( + # @param a [nil] + def foo(a); end + + # @param a [NilClass] + def bar(a) + foo(a) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not complain when passing true to TrueClass parameter' do + checker = type_checker(%( + # @param a [TrueClass] + def foo(a); end + + foo(true) + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not complain when passing TrueClass to true parameter' do + checker = type_checker(%( + # @param a [true] + def foo(a); end + + # @param a [TrueClass] + def bar(a) + foo(a) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end end end diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index 49729364b..b10bbd42c 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -372,5 +372,23 @@ class Bar < Foo; end )) expect(checker.problems).to be_empty end + + it 'matches "supertype" of explicit literal for assignability' do + checker = type_checker(%( + def nil_assignment? + false + end + )) + expect(checker.problems).to be_empty + end + + it 'matches "supertype" of inferred literal for assignability' do + checker = type_checker(%( + def nil_assignment? + 'foo'.nil? # infers as 'false' + end + )) + expect(checker.problems.map(&:message)).to be_empty + end end end From 72eb43f1298548e6fc11ec114d4c2b9882b9aed9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 26 May 2025 12:21:11 -0400 Subject: [PATCH 039/116] Mark asserts pending different PRs --- lib/solargraph.rb | 4 ++++ lib/solargraph/pin/base.rb | 2 +- spec/pin/local_variable_spec.rb | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 07af50508..3b19e2f84 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -56,6 +56,10 @@ class InvalidRubocopVersionError < RuntimeError; end # used in the future to allow configurable asserts mixes for # different situations. def self.asserts_on?(type) + # Pending https://github.com/castwide/solargraph/pull/950 + return false if type == :combine_with_visibility + # Pending https://github.com/castwide/solargraph/pull/947 + return false if type == :combine_with_closure_name if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? false elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 49d80359f..272d1c10b 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -228,7 +228,7 @@ def choose_pin_attr_with_same_name(other, attr) val1 = send(attr) val2 = other.send(attr) if val1&.name != val2&.name - Solargraph.assert_or_log(:combine_with, + Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end [val1, val2].compact.min diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index b4f676157..0e9e81871 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -30,7 +30,8 @@ class Foo # should indicate which one should override in the range situation end - it "asserts on attempt to merge namespace changes" do + # Pending https://github.com/castwide/solargraph/pull/947 + xit "asserts on attempt to merge namespace changes" do map1 = Solargraph::SourceMap.load_string(%( class Foo foo = 'foo' @@ -46,7 +47,7 @@ class Bar # set env variable 'FOO' to 'true' in block with_env_var('SOLARGRAPH_ASSERTS', 'on') do - expect(Solargraph.asserts_on?(:combine_with)).to be true + expect(Solargraph.asserts_on?(:combine_with_closure_name)).to be true expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) end end From 6141edea1b37fbaa6908be184039a6c7678fdd9b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 26 May 2025 12:35:24 -0400 Subject: [PATCH 040/116] Tweak README text --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fd65e0804..0acaa61fe 100755 --- a/README.md +++ b/README.md @@ -63,19 +63,21 @@ The RSpec framework is supported via [solargraph-rspec](https://github.com/lekem **Note: Before version 0.53.0, it was recommended to run `yard gems` periodically or automate it with `yard config` to ensure that Solargraph had access to gem documentation. These steps are no longer necessary. Solargraph maintains its own gem documentation cache independent of the yardocs in your gem installations.** -When editing code, a `require` call that references a gem will pull the documentation into the code maps and include the gem's API in code completion and intellisense. +When editing code, a `require` call that references a gem will pull the documentation into the code maps and include the gem's API in code completion and intellisense. Solargraph automatically generates code maps from installed gems, based on the YARD or RBS type information inside the gem. You can also eagerly cache gem documentation with the `solargraph gems` command. -Solargraph automatically generates code maps from installed gems. You can also manage your cached gem documentation with the `solargraph gems` command. +If your project automatically requires bundled gems (e.g., `require 'bundler/require'`), Solargraph will add all of the Gemfile's default dependencies to the map. -To combine this YARD and Rdoc information with RBS, use [gem\_rbs\_collection](https://github.com/ruby/gem_rbs_collection) -to install RBS types for Rails: +To ensure you have types for gems which contain neither RBS nor YARD +information, use +[gem\_rbs\_collection](https://github.com/ruby/gem_rbs_collection) to +install a community-supported set of RBS types for various gems: ```sh bundle exec rbs collection init bundle exec rbs collection install ``` -If your project automatically requires bundled gems (e.g., `require 'bundler/require'`), Solargraph will add all of the Gemfile's default dependencies to the map. +Once installed, you can also insert your own local overrides and definitions in RBS in a directory configured in the `rbs_collection.yaml` that the above commands create. ### Type Checking From 16ad2c19354717b99533d36906f40afade13516f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 10:50:06 -0400 Subject: [PATCH 041/116] Add ::ClassMethods support --- lib/solargraph/api_map.rb | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index b74de2b93..656c02858 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -667,20 +667,26 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end else store.get_includes(fqns).reverse.each do |include_tag| - logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{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(include_tag) # ActiveSupport::Concern is syntactic sugar for a common - # pattern to provide virtual class method - i.e., if Foo - # includes Bar and Bar is a module using this - # pattern, Bar can supply class methods which will also - # appear under Foo. + # pattern to include class methods while mixing-in a Module # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html - included_class_pins = inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) - # activesupport_concern_pins = included_class_pins.select { |p| p.virtual_class_method? } - # result.concat activesupport_concern_pins - result.concat included_class_pins # TODO remove this line once we have activesupport::concern support + logger.debug { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + if module_extends.include? 'ActiveSupport::Concern' + rooted_include_tag = qualify(include_tag, rooted_tag) + 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 + included_classmethods_pins = inner_get_methods_from_reference(rooted_include_tag + "::ClassMethods", 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}" } From f3468922f78c57e99bd743a2e38d4d8f3a8d0327 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 11:31:20 -0400 Subject: [PATCH 042/116] Avoid bad sources of return_type when possible --- lib/solargraph/pin/base.rb | 10 +++++++++- lib/solargraph/pin/parameter.rb | 8 ++++++++ lib/solargraph/pin/signature.rb | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 272d1c10b..b87479724 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -10,7 +10,6 @@ class Base include Documenting include Logging - # @return [YARD::CodeObjects::Base] attr_reader :code_object @@ -128,6 +127,10 @@ def combine_return_type(other) other.return_type elsif other.return_type.undefined? return_type + elsif dodgy_return_type_source? && !other.dodgy_return_type_source? + other.return_type + elsif other.dodgy_return_type_source? && !dodgy_return_type_source? + return_type else all_items = return_type.items + other.return_type.items if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_tag == context.rooted_tag } @@ -138,6 +141,11 @@ def combine_return_type(other) end end + def dodgy_return_type_source? + # uses a lot of 'Object' instead of 'self' + location&.filename&.include?('core_ext/object/') + end + def <=>(p1) return nil unless p1.is_a?(self.class) return 0 if self == p1 diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index db0cdc113..b361b853c 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -17,6 +17,14 @@ def initialize decl: :arg, asgn_code: nil, **splat @decl = decl end + def type_location + super || closure&.type_location + end + + def location + super || closure&.type_location + end + def combine_with(other, attrs={}) new_attrs = { decl: assert_same(other, :decl), diff --git a/lib/solargraph/pin/signature.rb b/lib/solargraph/pin/signature.rb index 886f14705..7c4e34131 100644 --- a/lib/solargraph/pin/signature.rb +++ b/lib/solargraph/pin/signature.rb @@ -15,6 +15,18 @@ def identity attr_writer :closure + def dodgy_return_type_source? + super || closure&.dodgy_return_type_source? + end + + def type_location + super || closure&.type_location + end + + def location + super || closure&.location + end + def typify api_map if return_type.defined? qualified = return_type.qualify(api_map, closure.namespace) From e29a28c8ceba188554b3016656c11a2da24c1c1a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 12:37:10 -0400 Subject: [PATCH 043/116] Prefer method pins wtih non-undefined signatures --- lib/solargraph/pin/method.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index a399584d0..24456a120 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -56,8 +56,20 @@ def combine_visibility(other) end end + def combine_signatures(other) + all_undefined = signatures.all? { |sig| sig.return_type.undefined? } + other_all_undefined = other.signatures.all? { |sig| sig.return_type.undefined? } + if all_undefined && !other_all_undefined + other.signatures + elsif other_all_undefined && !all_undefined + signatures + else + combine_all_signature_pins(*signatures, *other.signatures) + end + end + def combine_with(other, attrs = {}) - sigs = combine_all_signature_pins(*(self.signatures + other.signatures)).clone.freeze + sigs = combine_signatures(other) parameters = if sigs.length > 0 [].freeze else From b464aea626c697d77232bb30f47e938d1ab6f7bc Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 19:13:05 -0400 Subject: [PATCH 044/116] Fix merge --- lib/solargraph/pin/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 1e79397a7..3ac58a608 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -43,7 +43,6 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @name = name @source = source @comments = comments - @source = source end # @return [String] From cb55bc88834c7dc64f69c129e6f196070662e097 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 2 Jun 2025 11:48:57 -0400 Subject: [PATCH 045/116] Ensure overrides apply to all pins for a path, since we use all --- lib/solargraph/api_map/index.rb | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index dcb273071..810600534 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -3,6 +3,8 @@ module Solargraph class ApiMap class Index + include Logging + # @param pins [Array] def initialize pins = [] catalog pins @@ -132,21 +134,24 @@ def store_parametric_reference(hash, reference_pin) # @return [void] def map_overrides pins_by_class(Pin::Reference::Override).each do |ovr| - pin = path_pin_hash[ovr.name].first - next if pin.nil? - new_pin = if pin.path.end_with?('#initialize') - path_pin_hash[pin.path.sub(/#initialize/, '.new')].first - end - (ovr.tags.map(&:tag_name) + ovr.delete).uniq.each do |tag| - pin.docstring.delete_tags tag - new_pin.docstring.delete_tags tag if new_pin - end - ovr.tags.each do |tag| - pin.docstring.add_tag(tag) - redefine_return_type pin, tag - if new_pin - new_pin.docstring.add_tag(tag) - redefine_return_type new_pin, tag + logger.debug { "ApiMap::Index#map_overrides: Looking at override #{ovr} for #{ovr.name}" } + pins = path_pin_hash[ovr.name] + logger.debug { "ApiMap::Index#map_overrides: pins for path=#{ovr.name}: #{pins}" } + pins.each do |pin| + new_pin = if pin.path.end_with?('#initialize') + path_pin_hash[pin.path.sub(/#initialize/, '.new')].first + end + (ovr.tags.map(&:tag_name) + ovr.delete).uniq.each do |tag| + pin.docstring.delete_tags tag + new_pin.docstring.delete_tags tag if new_pin + end + ovr.tags.each do |tag| + pin.docstring.add_tag(tag) + redefine_return_type pin, tag + if new_pin + new_pin.docstring.add_tag(tag) + redefine_return_type new_pin, tag + end end end end From 5a8e828214b6efcf18b224e285c60b32d76a2181 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 2 Jun 2025 11:50:49 -0400 Subject: [PATCH 046/116] Reset method parameters and block if needed in reset_generated! --- lib/solargraph/pin/method.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 24456a120..6792b9b48 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -106,7 +106,13 @@ def transform_types(&transform) def reset_generated! super - return_type = nil unless signatures.empty? + unless signatures.empty? + return_type = nil + @block = :undefined + parameters = [] + end + block&.reset_generated! + @signatures&.each(&:reset_generated!) signature_help = nil documentation = nil end From 620e62c26c87e1a51b04359375494871dee76cef Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 2 Jun 2025 12:08:29 -0400 Subject: [PATCH 047/116] Block debugging --- lib/solargraph/pin/block.rb | 18 +++++++++++++++--- lib/solargraph/pin/reference/override.rb | 4 ++++ lib/solargraph/source/chain.rb | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index d582d338b..e06e94fae 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -58,15 +58,23 @@ def typify_parameters(api_map) 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 @@ -85,6 +93,7 @@ def typify_parameters(api_map) logger.debug { "Block#typify_parameters() - param_types=#{param_types.map(&:rooted_tags)}" } end end + logger.debug { "Block#typify_parameters(): methods provided no information" } out = parameters.map { ComplexType::UNDEFINED } logger.debug { "Block#typify_parameters() => #{out.map(&:rooted_tags)}" } out @@ -105,7 +114,10 @@ def maybe_rebind api_map 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) diff --git a/lib/solargraph/pin/reference/override.rb b/lib/solargraph/pin/reference/override.rb index 0f623aa4d..cf47c2305 100644 --- a/lib/solargraph/pin/reference/override.rb +++ b/lib/solargraph/pin/reference/override.rb @@ -10,6 +10,10 @@ class Override < Reference # @return [::Array] attr_reader :delete + def inner_desc + super + ", tags=#{tags.inspect}, delete=#{delete.inspect}" + end + def initialize location, name, tags, delete = [] super(location: location, name: name) @tags = tags diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 111a06f91..dc9613a19 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -128,7 +128,7 @@ def define api_map, name_pin, locals end links.last.last_context = working_pin out = links.last.resolve(api_map, working_pin, locals) - logger.debug { "Chain#define(name_pin=#{name_pin.desc}, links=#{links.map(&:desc)}, locals=#{locals}) => #{out}" } + logger.debug { "Chain#define(links=#{links.map(&:desc)}, name_pin=#{name_pin.desc}, locals=#{locals}) => #{out}" } out end From 784eb270dfd1b4db0c2d4937e9f72b52eeb6c518 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 7 Jun 2025 07:34:23 -0400 Subject: [PATCH 048/116] rbs_path -> rbs_collection_path --- lib/solargraph/doc_map.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index ecd696259..9f1147d2b 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -20,11 +20,11 @@ class DocMap # @param requires [Array] # @param preferences [Array] - # @param rbs_path [String, Pathname, nil] - def initialize(requires, preferences, rbs_path = nil) + # @param rbs_collection_path [String, Pathname, nil] + def initialize(requires, preferences, rbs_collection_path = nil) @requires = requires.compact @preferences = preferences.compact - @rbs_path = rbs_path + @rbs_collection_path = rbs_collection_path generate end @@ -118,7 +118,7 @@ def try_gem_in_memory gemspec # @param gemspec [Gem::Specification] def update_from_collection gemspec, gempins - unless @rbs_path && File.directory?(@rbs_path) + unless @rbs_collection_path && File.directory?(@rbs_collection_path) logger.debug { "DocMap#update_from_collection: No collection" } return gempins end @@ -128,13 +128,13 @@ def update_from_collection gemspec, gempins return GemPins.combine(gempins, rbs_map) end - rbs_map = RbsMap.new(gemspec.name, gemspec.version, directories: [@rbs_path]) + rbs_map = RbsMap.new(gemspec.name, gemspec.version, directories: [@rbs_collection_path]) if rbs_map.resolved? logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" return GemPins.combine(gempins, rbs_map) end - rbs_map = RbsMap.new(gemspec.name, nil, directories: [@rbs_path]) + rbs_map = RbsMap.new(gemspec.name, nil, directories: [@rbs_collection_path]) if rbs_map.resolved? logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" return GemPins.combine(gempins, rbs_map) From b65b2badbe503babd35215adfaa1c7d2ff5d16d9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 7 Jun 2025 07:47:31 -0400 Subject: [PATCH 049/116] Recreate docmap when rbs_collection_path changes --- lib/solargraph/api_map.rb | 5 ++++- lib/solargraph/doc_map.rb | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 40b011f9c..02fe1d643 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -93,7 +93,10 @@ def catalog bench implicit.merge map.environ end unresolved_requires = (bench.external_requires + implicit.requires + bench.workspace.config.required).to_a.compact.uniq - if @unresolved_requires != unresolved_requires || @doc_map&.uncached_gemspecs&.any? + recreate_docmap = @unresolved_requires != unresolved_requires || + @doc_map&.uncached_gemspecs&.any? || + docmap.rbs_collection_path != bench.workspace.rbs_collection_path + if recreate_docmap @doc_map = DocMap.new(unresolved_requires, [], bench.workspace.rbs_collection_path) # @todo Implement gem preferences @unresolved_requires = unresolved_requires end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 9f1147d2b..8b7cdbc45 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -18,6 +18,8 @@ class DocMap # @return [Array] attr_reader :uncached_gemspecs + attr_reader :rbs_collection_path + # @param requires [Array] # @param preferences [Array] # @param rbs_collection_path [String, Pathname, nil] @@ -38,11 +40,14 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Hash{Gem::Specification => Array[Pin::Base]}] - def self.gems_in_memory + def self.all_gems_in_memory @gems_in_memory ||= {} end + def gems_in_memory + self.class.all_gems_in_memory[rbs_collection_path] ||= {} + end + # @return [Set] def dependencies @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set @@ -85,7 +90,7 @@ def try_cache gemspec if Cache.exist?(cache_file) cached = Cache.load(cache_file) gempins = update_from_collection(gemspec, cached) - self.class.gems_in_memory[gemspec] = gempins + gems_in_memory[gemspec] = gempins @pins.concat gempins else logger.debug "No pin cache for #{gemspec.name} #{gemspec.version}" @@ -109,7 +114,7 @@ def try_stdlib_map path # @param gemspec [Gem::Specification] # @return [Boolean] def try_gem_in_memory gemspec - gempins = DocMap.gems_in_memory[gemspec] + gempins = gems_in_memory[gemspec] return false unless gempins logger.debug { "Found #{gemspec.name} #{gemspec.version} in memory" } @pins.concat gempins From e3042f6d2a2d84899911f20d41866d73bef55398 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 7 Jun 2025 07:55:06 -0400 Subject: [PATCH 050/116] Fix doc_map reference --- lib/solargraph/api_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 02fe1d643..3f53b803c 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -95,7 +95,7 @@ def catalog bench unresolved_requires = (bench.external_requires + implicit.requires + bench.workspace.config.required).to_a.compact.uniq recreate_docmap = @unresolved_requires != unresolved_requires || @doc_map&.uncached_gemspecs&.any? || - docmap.rbs_collection_path != bench.workspace.rbs_collection_path + @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path if recreate_docmap @doc_map = DocMap.new(unresolved_requires, [], bench.workspace.rbs_collection_path) # @todo Implement gem preferences @unresolved_requires = unresolved_requires From 2e09267ed06f4a5c35303c4c3f94fe92a777f22d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:12:10 -0400 Subject: [PATCH 051/116] Cache combined pins on disk as well --- lib/solargraph.rb | 2 +- lib/solargraph/api_map.rb | 42 +++- lib/solargraph/cache.rb | 77 ------ lib/solargraph/doc_map.rb | 238 +++++++++++++----- lib/solargraph/gem_pins.rb | 44 ++-- .../message/extended/check_gem_version.rb | 2 + lib/solargraph/library.rb | 8 +- .../parser_gem/node_processors/casgn_node.rb | 4 +- .../node_processors/namespace_node.rb | 4 +- lib/solargraph/pin_cache.rb | 159 ++++++++++++ lib/solargraph/rbs_map.rb | 74 ++++-- lib/solargraph/rbs_map/conversions.rb | 10 + lib/solargraph/rbs_map/core_map.rb | 27 +- lib/solargraph/rbs_map/stdlib_map.rb | 25 +- lib/solargraph/shell.rb | 24 +- lib/solargraph/workspace.rb | 14 +- lib/solargraph/yardoc.rb | 14 +- spec/doc_map_spec.rb | 8 +- spec/gem_pins_spec.rb | 9 +- spec/rbs_map_spec.rb | 4 +- spec/type_checker/levels/normal_spec.rb | 5 +- spec/type_checker/levels/strict_spec.rb | 5 +- 22 files changed, 546 insertions(+), 253 deletions(-) delete mode 100644 lib/solargraph/cache.rb create mode 100644 lib/solargraph/pin_cache.rb diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 352b0eaad..f7b260ca7 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -47,7 +47,7 @@ class InvalidRubocopVersionError < RuntimeError; end autoload :Parser, 'solargraph/parser' autoload :RbsMap, 'solargraph/rbs_map' autoload :GemPins, 'solargraph/gem_pins' - autoload :Cache, 'solargraph/cache' + autoload :PinCache, 'solargraph/pin_cache' dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 3f53b803c..029260419 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -94,11 +94,14 @@ def catalog bench end unresolved_requires = (bench.external_requires + implicit.requires + bench.workspace.config.required).to_a.compact.uniq recreate_docmap = @unresolved_requires != unresolved_requires || - @doc_map&.uncached_gemspecs&.any? || + @doc_map&.uncached_yard_gemspecs&.any? || + @doc_map&.uncached_rbs_collection_gemspecs&.any? || @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path if recreate_docmap - @doc_map = DocMap.new(unresolved_requires, [], bench.workspace.rbs_collection_path) # @todo Implement gem preferences - @unresolved_requires = unresolved_requires + @doc_map = DocMap.new(unresolved_requires, [], + bench.workspace.rbs_collection_path, + bench.workspace.rbs_collection_config_path) # @todo Implement gem preferences + @unresolved_requires = @doc_map.unresolved_requires end @cache.clear if store.update(@@core_map.pins, @doc_map.pins, implicit.pins, iced_pins, live_pins) @missing_docs = [] # @todo Implement missing docs @@ -117,6 +120,16 @@ def uncached_gemspecs @doc_map&.uncached_gemspecs || [] end + # @return [::Array] + def uncached_rbs_collection_gemspecs + @doc_map.uncached_rbs_collection_gemspecs + end + + # @return [::Array] + def uncached_yard_gemspecs + @doc_map.uncached_yard_gemspecs + end + # @return [Array] def core_pins @@core_map.pins @@ -171,6 +184,18 @@ def self.load directory api_map end + def cache_all!(out) + @doc_map.cache_all!(out) + end + + def cache_gem(gemspec, rebuild: false, out: nil) + @doc_map.cache(gemspec, rebuild: rebuild, out: out) + end + + class << self + include Logging + end + # Create an ApiMap with a workspace in the specified directory and cache # any missing gems. # @@ -183,13 +208,12 @@ def self.load directory # @return [ApiMap] def self.load_with_cache directory, out = IO::NULL api_map = load(directory) - return api_map if api_map.uncached_gemspecs.empty? - - api_map.uncached_gemspecs.each do |gemspec| - out.puts "Caching gem #{gemspec.name} #{gemspec.version}" - pins = GemPins.build(gemspec) - Solargraph::Cache.save('gems', "#{gemspec.name}-#{gemspec.version}.ser", pins) + if api_map.uncached_gemspecs.empty? + logger.info { "All gems cached for #{directory}" } + return api_map end + + api_map.cache_all!(out) load(directory) end diff --git a/lib/solargraph/cache.rb b/lib/solargraph/cache.rb deleted file mode 100644 index 009182d3b..000000000 --- a/lib/solargraph/cache.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'fileutils' -require 'rbs' - -module Solargraph - module Cache - class << self - # The base directory where cached documentation is installed. - # - # @return [String] - def base_dir - # The directory is not stored in a variable so it can be overridden - # in specs. - ENV['SOLARGRAPH_CACHE'] || - (ENV['XDG_CACHE_HOME'] ? File.join(ENV['XDG_CACHE_HOME'], 'solargraph') : nil) || - File.join(Dir.home, '.cache', 'solargraph') - end - - # The working directory for the current Ruby, RBS, and Solargraph versions. - # - # @return [String] - def work_dir - # The directory is not stored in a variable so it can be overridden - # in specs. - File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") - end - - # Append the given path to the current cache directory (`work_dir`). - # - # @example - # Cache.join('date-3.4.1.ser') - # - # @param path [Array] - # @return [String] - def join *path - File.join(work_dir, *path) - end - - # @param path [Array] - # @return [Array, nil] - def load *path - file = join(*path) - return nil unless File.file?(file) - Marshal.load(File.read(file, mode: 'rb')) - rescue StandardError => e - Solargraph.logger.warn "Failed to load cached file #{file}: [#{e.class}] #{e.message}" - FileUtils.rm_f file - nil - end - - def exist? *path - File.file? join(*path) - end - - # @param path [Array] - # @param pins [Array] - # @return [void] - def save *path, pins - file = File.join(work_dir, *path) - base = File.dirname(file) - FileUtils.mkdir_p base unless File.directory?(base) - ser = Marshal.dump(pins) - File.write file, ser, mode: 'wb' - end - - # @return [void] - # @param path [Array] - def uncache *path - FileUtils.rm_rf File.join(work_dir, *path), secure: true - end - - # @return [void] - def clear - FileUtils.rm_rf base_dir, secure: true - end - end - end -end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 8b7cdbc45..7116776ab 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'pathname' +require 'benchmark' + module Solargraph # A collection of pins generated from required gems. # @@ -16,18 +19,67 @@ class DocMap attr_reader :pins # @return [Array] - attr_reader :uncached_gemspecs + def uncached_gemspecs + (uncached_yard_gemspecs + uncached_rbs_collection_gemspecs).sort. + uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" } + end + + # @return [Array] + attr_reader :uncached_yard_gemspecs + + # @return [Array] + attr_reader :uncached_rbs_collection_gemspecs attr_reader :rbs_collection_path + attr_reader :rbs_collection_config_path # @param requires [Array] # @param preferences [Array] # @param rbs_collection_path [String, Pathname, nil] - def initialize(requires, preferences, rbs_collection_path = nil) + def initialize(requires, preferences, rbs_collection_path = nil, rbs_collection_config_path = nil) + raise "Please provide rbs_collection_config_path if you provide rbs_collection_path" if rbs_collection_path && rbs_collection_config_path.nil? @requires = requires.compact @preferences = preferences.compact @rbs_collection_path = rbs_collection_path - generate + @rbs_collection_config_path = rbs_collection_config_path + load_serialized_gem_pins + end + + def cache_all!(out) + gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') + out.puts "Caching pins for gems: #{gem_desc.inspect}" unless uncached_gemspecs.empty? + logger.warn { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } + logger.warn { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } + load_serialized_gem_pins + uncached_gemspecs.each do |gemspec| + out.puts "Caching pins for gem #{gemspec.name}:#{gemspec.version}" + cache(gemspec, out: out) + end + load_serialized_gem_pins + @uncached_rbs_collection_gemspecs = [] + @uncached_yard_gemspecs = [] + end + + def cache_yard_pins(gemspec, out) + pins = GemPins.build_yard_pins(gemspec) + PinCache.serialize_yard_gem(gemspec, pins) + out.puts "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" unless pins.empty? + end + + def cache_rbs_collection_pins(gemspec, out) + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + pins = rbs_map.pins + rbs_version_cache_key = rbs_map.cache_key + # cache pins even if result is zero, so we don't retry building pins + pins ||= [] + PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins) + out.puts "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? + end + + # @param gemspec [Gem::Specification] + def cache(gemspec, rebuild: false, out: nil) + 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 end # @return [Array] @@ -40,12 +92,24 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - def self.all_gems_in_memory - @gems_in_memory ||= {} + def self.all_yard_gems_in_memory + @yard_gems_in_memory ||= {} + end + + def self.all_rbs_collection_gems_in_memory + @rbs_collection_gems_in_memory ||= {} + end + + def yard_pins_in_memory + self.class.all_yard_gems_in_memory + end + + def rbs_collection_pins_in_memory + self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {} end - def gems_in_memory - self.class.all_gems_in_memory[rbs_collection_path] ||= {} + def combined_pins_in_memory + @combined_pins_in_memory ||= {} end # @return [Set] @@ -55,21 +119,29 @@ def dependencies private - # @return [void] - def generate + def load_serialized_gem_pins @pins = [] - @uncached_gemspecs = [] - required_gems_map.each do |path, gemspecs| - if gemspecs.nil? - try_stdlib_map path - else - gemspecs.each do |gemspec| - try_cache gemspec - end + @uncached_yard_gemspecs = [] + @uncached_rbs_collection_gemspecs = [] + with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v } + paths = Hash[without_gemspecs].keys + gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a + + paths.each do |path| + rbs_pins = deserialize_stdlib_rbs_map path + end + + logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." } + time = Benchmark.measure do + gemspecs.each do |gemspec| + pins = deserialize_combined_pin_cache gemspec + @pins.concat pins if pins end end - dependencies.each { |dep| try_cache dep } - @uncached_gemspecs.uniq! + logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" } + @uncached_yard_gemspecs.uniq! + @uncached_rbs_collection_gemspecs.uniq! + nil end # @return [Hash{String => Array}] @@ -82,73 +154,101 @@ def preference_map @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } end + # @param gemspec [Gem::Specification] + # @return [Array] + def deserialize_yard_pin_cache gemspec + if yard_pins_in_memory.key?([gemspec.name, gemspec.version]) + return yard_pins_in_memory[[gemspec.name, gemspec.version]] + end + + cached = PinCache.deserialize_yard_gem(gemspec) + if cached + logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } + yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached + cached + else + logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" + @uncached_yard_gemspecs.push gemspec + nil + end + end + # @param gemspec [Gem::Specification] # @return [void] - def try_cache gemspec - return if try_gem_in_memory(gemspec) - cache_file = File.join('gems', "#{gemspec.name}-#{gemspec.version}.ser") - if Cache.exist?(cache_file) - cached = Cache.load(cache_file) - gempins = update_from_collection(gemspec, cached) - gems_in_memory[gemspec] = gempins - @pins.concat gempins + def deserialize_combined_pin_cache(gemspec) + unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil? + return combined_pins_in_memory[[gemspec.name, gemspec.version]] + end + + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + rbs_version_cache_key = rbs_map.cache_key + + cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key) + if cached + logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" } + combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached + return combined_pins_in_memory[[gemspec.name, gemspec.version]] + end + + rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key + + yard_pins = deserialize_yard_pin_cache gemspec + + if !rbs_collection_pins.nil? && !yard_pins.nil? + logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" } + combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) + PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) + combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins + logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" } + return combined_pins + end + + if !yard_pins.nil? + logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" } + combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins + return combined_pins_in_memory[[gemspec.name, gemspec.version]] + elsif !rbs_collection_pins.nil? + logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" } + combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins + return combined_pins_in_memory[[gemspec.name, gemspec.version]] else - logger.debug "No pin cache for #{gemspec.name} #{gemspec.version}" - @uncached_gemspecs.push gemspec + logger.warn { "No pins found for gem #{gemspec.name}:#{gemspec.version}" } + return nil end end # @param path [String] require path that might be in the RBS stdlib collection # @return [void] - def try_stdlib_map path + def deserialize_stdlib_rbs_map path map = RbsMap::StdlibMap.load(path) if map.resolved? logger.debug { "Loading stdlib pins for #{path}" } @pins.concat map.pins + logger.info { "Loaded #{map.pins.length} stdlib pins for #{path}" } + map.pins else # @todo Temporarily ignoring unresolved `require 'set'` - logger.debug { "Require path #{path} could not be resolved" } unless path == 'set' + logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' + nil end end - # @param gemspec [Gem::Specification] - # @return [Boolean] - def try_gem_in_memory gemspec - gempins = gems_in_memory[gemspec] - return false unless gempins - logger.debug { "Found #{gemspec.name} #{gemspec.version} in memory" } - @pins.concat gempins - true - end - - # @param gemspec [Gem::Specification] - def update_from_collection gemspec, gempins - unless @rbs_collection_path && File.directory?(@rbs_collection_path) - logger.debug { "DocMap#update_from_collection: No collection" } - return gempins - end - rbs_map = RbsMap.new(gemspec.name, gemspec.version) - if rbs_map.resolved? - logger.info { "DocMap#update_from_collection: Resolved #{gemspec.name} to RBS exported by gem" } - return GemPins.combine(gempins, rbs_map) - end - - rbs_map = RbsMap.new(gemspec.name, gemspec.version, directories: [@rbs_collection_path]) - if rbs_map.resolved? - logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" - return GemPins.combine(gempins, rbs_map) - end - - rbs_map = RbsMap.new(gemspec.name, nil, directories: [@rbs_collection_path]) - if rbs_map.resolved? - logger.info "Updating #{gemspec.name} #{gemspec.version} from collection" - return GemPins.combine(gempins, rbs_map) + # @return [Array, nil] + def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key + return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key]) + cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key) + if cached + logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty? + rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached + cached + else + logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}" + @uncached_rbs_collection_gemspecs.push gemspec + nil end - - logger.debug { "DocMap#update_from_collection: No collection found for #{gemspec.name}" } - gempins end + # @param gemspec [Gem::Specification] # @param path [String] # @return [::Array, nil] def resolve_path_to_gemspecs path @@ -220,7 +320,7 @@ def fetch_dependencies gemspec dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) deps.merge fetch_dependencies(dep) if deps.add?(dep) rescue Gem::MissingSpecError - Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found." + Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems." end.to_a end @@ -229,5 +329,9 @@ def fetch_dependencies gemspec def only_runtime_dependencies gemspec gemspec.dependencies - gemspec.development_dependencies end + + def inspect + self.class.inspect + end end end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index fb23655ed..6fdc06d11 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -7,29 +7,37 @@ module Solargraph # documentation. # module GemPins - # Build an array of pins from a gem specification. The process starts with - # YARD, enhances the resulting pins with RBS definitions, and appends RBS - # pins that don't exist in the YARD mapping. - # + class << self + include Logging + end + + def log_level + :debug + end + # @param gemspec [Gem::Specification] # @return [Array] - def self.build(gemspec) - yard_pins = build_yard_pins(gemspec) - rbs_map = RbsMap.from_gemspec(gemspec) - combine yard_pins, rbs_map + def self.build_yard_pins(gemspec) + Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec) + yardoc = Yardoc.load!(gemspec) + YardMap::Mapper.new(yardoc, gemspec).map end + # Build an array of pins by combining YARD and RBS + # information. + # # @param yard_pins [Array] # @param rbs_map [RbsMap] # @return [Array] - def self.combine(yard_pins, rbs_map) + def self.combine(yard_pins, rbs_pins) in_yard = Set.new + rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins) combined = yard_pins.map do |yard| in_yard.add yard.path next yard unless yard.is_a?(Pin::Method) - rbs = rbs_map.path_pin(yard.path, Pin::Method) - next yard unless rbs + rbs = rbs_api_map.get_path_pins(yard.path).first + next yard unless rbs && yard.is_a?(Pin::Method) # @sg-ignore yard.class.new( @@ -45,21 +53,15 @@ def self.combine(yard_pins, rbs_map) return_type: best_return_type(rbs.return_type, yard.return_type) ) end - in_rbs = rbs_map.pins.reject { |pin| in_yard.include?(pin.path) } - combined + in_rbs + in_rbs = rbs_pins.reject { |pin| in_yard.include?(pin.path) } + out = combined + in_rbs + logger.debug { "GemPins#combine: Returning #{out.length} combined pins" } + out end class << self private - # @param gemspec [Gem::Specification] - # @return [Array] - def build_yard_pins(gemspec) - Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec) - yardoc = Yardoc.load!(gemspec) - YardMap::Mapper.new(yardoc, gemspec).map - end - # Select the first defined type. # # @param choices [Array] diff --git a/lib/solargraph/language_server/message/extended/check_gem_version.rb b/lib/solargraph/language_server/message/extended/check_gem_version.rb index c421ec63a..2e80f40c6 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -83,6 +83,8 @@ def available @fetched = true begin @available ||= begin + # @sg-ignore + # @type [Gem::Dependency, nil] tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first if tuple.nil? @error = 'An error occurred fetching the gem data' diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 76d8e35b5..bb7f98e41 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -586,8 +586,9 @@ def cache_errors # @return [void] def cache_next_gemspec - return if @cache_progress - spec = api_map.uncached_gemspecs.find { |spec| !cache_errors.include?(spec) } + return if @cache_progres + spec = (api_map.uncached_yard_gemspecs + api_map.uncached_rbs_collection_gemspecs). + find { |spec| !cache_errors.include?(spec) } return end_cache_progress unless spec pending = api_map.uncached_gemspecs.length - cache_errors.length - 1 @@ -654,7 +655,8 @@ def sync_catalog api_map.catalog bench source_map_hash.values.each { |map| find_external_requires(map) } logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" - logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs" + logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" + logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" cache_next_gemspec @sync_count = 0 end diff --git a/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb index 7646973a5..55a3c4717 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb @@ -29,8 +29,8 @@ def process_constant_assignment process_children end - # TODO: Move this out of [CasgnNode] once [Solargraph::Parser::NodeProcessor] supports - # multiple processors. + # @todo Move this out of [CasgnNode] once [Solargraph::Parser::NodeProcessor] supports + # multiple processors. def process_struct_assignment processor_klass = Convention::StructDefinition::NodeProcessors::StructNode processor = processor_klass.new(node, region, pins, locals) 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 529f1f278..53b65bc04 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -43,8 +43,8 @@ def process_namespace(superclass_name) process_children region.update(closure: nspin, visibility: :public) end - # TODO: Move this out of [NamespaceNode] once [Solargraph::Parser::NodeProcessor] supports - # multiple processors. + # @todo Move this out of [NamespaceNode] once [Solargraph::Parser::NodeProcessor] supports + # multiple processors. def process_struct_definition processor_klass = Convention::StructDefinition::NodeProcessors::StructNode processor = processor_klass.new(node, region, pins, locals) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb new file mode 100644 index 000000000..5721b4236 --- /dev/null +++ b/lib/solargraph/pin_cache.rb @@ -0,0 +1,159 @@ +require 'fileutils' +require 'rbs' + +module Solargraph + module PinCache + class << self + include Logging + + # The base directory where cached YARD documentation and serialized pins are serialized + # + # @return [String] + def base_dir + # The directory is not stored in a variable so it can be overridden + # in specs. + ENV['SOLARGRAPH_CACHE'] || + (ENV['XDG_CACHE_HOME'] ? File.join(ENV['XDG_CACHE_HOME'], 'solargraph') : nil) || + File.join(Dir.home, '.cache', 'solargraph') + end + + # The working directory for the current Ruby, RBS, and Solargraph versions. + # + # @return [String] + def work_dir + # The directory is not stored in a variable so it can be overridden + # in specs. + File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") + end + + def yardoc_path gemspec + File.join(base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc") + end + + def stdlib_require_path require + File.join(work_dir, 'stdlib', "#{require}.ser") + end + + def deserialize_stdlib_require require + load(stdlib_require_path(require)) + end + + def serialize_stdlib_require require, pins + save(stdlib_require_path(require), pins) + end + + def core_path + 'core.ser' + end + + def deserialize_core + load(core_path) + end + + def serialize_core pins + save(core_path, pins) + end + + def yard_gem_path gemspec + File.join(work_dir, 'yard', "#{gemspec.name}-#{gemspec.version}.ser") + end + + def deserialize_yard_gem(gemspec) + load(yard_gem_path(gemspec)) + end + + def serialize_yard_gem(gemspec, pins) + save(yard_gem_path(gemspec), pins) + end + + def has_yard?(gemspec) + exist?(yard_gem_path(gemspec)) + end + + def rbs_collection_path(gemspec, hash) + File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") + end + + def deserialize_rbs_collection_gem(gemspec, hash) + load(rbs_collection_path(gemspec, hash)) + end + + def serialize_rbs_collection_gem(gemspec, hash, pins) + save(rbs_collection_path(gemspec, hash), pins) + end + + def combined_path(gemspec, hash) + File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") + end + + def serialize_combined_gem(gemspec, hash, pins) + save(combined_path(gemspec, hash), pins) + end + + def deserialize_combined_gem gemspec, hash + load(combined_path(gemspec, hash)) + end + + def has_rbs_collection?(gemspec, hash) + exist?(rbs_collection_path(gemspec, hash)) + end + + def uncache_core + uncache("core.ser") + end + + def uncache_stdlib + uncache("stdlib") + end + + def uncache_gem(gemspec, hash, out: nil) + uncache(yardoc_path(gemspec), out: out) + uncache(rbs_collection(gemspec, hash), out: out) + uncache(yard_gem_path(gemspec), out: out) + end + + # @return [void] + def clear + FileUtils.rm_rf base_dir, secure: true + end + + private + + # @param file [String] + # @return [Array, nil] + def load file + return nil unless File.file?(file) + Marshal.load(File.read(file, mode: 'rb')) + rescue StandardError => e + Solargraph.logger.warn "Failed to load cached file #{file}: [#{e.class}] #{e.message}" + FileUtils.rm_f file + nil + end + + def exist? *path + File.file? join(*path) + end + + # @param path [Array] + # @param pins [Array] + # @return [void] + def save file, pins + base = File.dirname(file) + FileUtils.mkdir_p base unless File.directory?(base) + ser = Marshal.dump(pins) + File.write file, ser, mode: 'wb' + logger.debug { "Cache#save: Saved #{pins.length} pins to #{file}" } + end + + # @return [void] + # @param path [Array] + def uncache *path, out: nil + path = File.join(work_dir, *path) + if File.exist?(path) + FileUtils.rm_rf path, secure: true + out.puts "Clearing pin cache in #{path}" unless out.nil? + end + end + end + end +end diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 2edfd4fb3..f6f9bd047 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -10,6 +10,7 @@ class RbsMap autoload :CoreFills, 'solargraph/rbs_map/core_fills' autoload :StdlibMap, 'solargraph/rbs_map/stdlib_map' + include Logging include Conversions # @type [Hash{String => RbsMap}] @@ -17,18 +18,66 @@ class RbsMap attr_reader :library + attr_reader :rbs_collection_paths + + attr_reader :rbs_collection_config_path + # @param library [String] # @param version [String, nil] - # @param directories [Array] - def initialize library, version = nil, directories: [] + # @param rbs_collection_paths [Array] + def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [] + if rbs_collection_config_path.nil? && !rbs_collection_paths.empty? + raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths' + end @library = library @version = version - @collection = nil - @directories = directories - loader = RBS::EnvironmentLoader.new(core_root: nil, repository: repository) + @rbs_collection_config_path = rbs_collection_config_path + @rbs_collection_paths = rbs_collection_paths add_library loader, library, version - return unless resolved? - load_environment_to_pins(loader) + end + + def loader + @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) + end + + # @return string representing the version of the RBS info fetched + # for the given library. Must change when the RBS info is + # updated upstream for the same library and version. May change + # if the config for where information comes form changes. + def cache_key + @hextdigest ||= begin + data_arr = rbs_collection_paths.map do |dir| + # TODO can I get this more directly upstream? + # rbs_collection_config_path = Pathname.new(dir).join("..", "rbs_collection.yaml") + collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) + gem_config = collection_config.gem(library) + next unless gem_config + gem_config.to_s + end.sort.compact + if data_arr.empty? + if resolved? + # definitely came from the gem itself and not elsewhere - + # only one version per gem + 'gem-export' + else + 'unresolved' + end + else + Digest::SHA1.hexdigest(data_arr.join) + end + end + end + + def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path + rbs_map = RbsMap.new(gemspec.name, gemspec.version, + rbs_collection_paths: [rbs_collection_path].compact, + rbs_collection_config_path: rbs_collection_config_path) + return rbs_map if rbs_map.resolved? + + # try any version of the gem in the collection + RbsMap.new(gemspec.name, nil, + rbs_collection_paths: [rbs_collection_path].compact, + rbs_collection_config_path: rbs_collection_config_path) end # @generic T @@ -52,7 +101,7 @@ def resolved? def repository @repository ||= RBS::Repository.new(no_stdlib: false).tap do |repo| - @directories.each { |dir| repo.add(Pathname.new(dir)) } + @rbs_collection_paths.each { |dir| repo.add(Pathname.new(dir)) } end end @@ -62,11 +111,6 @@ def self.load library @@rbs_maps_hash[library] ||= RbsMap.new(library) end - # @param gemspec [Gem::Specification] - def self.from_gemspec(gemspec) - RbsMap.new(gemspec.name, gemspec.version) - end - private # @param loader [RBS::EnvironmentLoader] @@ -75,10 +119,10 @@ def self.from_gemspec(gemspec) def add_library loader, library, version @resolved = if loader.has_library?(library: library, version: version) loader.add library: library, version: version - Solargraph.logger.info "#{short_name} successfully loaded library #{library}" + logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } true else - Solargraph.logger.info "#{short_name} failed to load library #{library}" + logger.info { "#{short_name} did not find data for library #{library}:#{version}" } false end end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 0aca728f1..38baa3cbf 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -22,7 +22,13 @@ def initialize visibility = :public # @return [Array] def pins + @loaded ||= false @pins ||= [] + if resolved? && !@loaded + @loaded = true + load_environment_to_pins(loader) + end + @pins end private @@ -37,6 +43,10 @@ def type_aliases def load_environment_to_pins(loader) environment = RBS::Environment.from_loader(loader).resolve_type_names cursor = pins.length + if environment.declarations.empty? + Solargraph.logger.warn "No RBS declarations found in environment for #{loader.core_root}." + return + end environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) } added_pins = pins[cursor..-1] added_pins.each { |pin| pin.source = :rbs } diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index d0c794f83..8cb70adc0 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -7,22 +7,35 @@ class RbsMap class CoreMap include Conversions + def resolved? + true + end + def initialize - cache = Cache.load('core.ser') + end + + def pins + return @pins if @pins + + @pins = [] + cache = PinCache.deserialize_core if cache - pins.replace cache + @pins.replace cache else - loader = RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) RBS::Environment.from_loader(loader).resolve_type_names load_environment_to_pins(loader) - pins.concat RbsMap::CoreFills::ALL - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + @pins.concat RbsMap::CoreFills::ALL + processed = ApiMap::Store.new(@pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } processed.each { |pin| pin.source = :rbs } - pins.replace processed + @pins.replace processed - Cache.save('core.ser', pins) + PinCache.serialize_core @pins end end + + def loader + @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) + end end end end diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index be8d8890c..d40d041cb 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -7,19 +7,32 @@ class RbsMap # Ruby stdlib pins # class StdlibMap < RbsMap + include Logging + # @type [Hash{String => RbsMap}] @stdlib_maps_hash = {} # @param library [String] def initialize library - cache = Cache.load('stdlib', "#{library}.ser") - if cache - pins.replace cache + super + end + + def gems + return @pins if @pins + cached_pins = PinCache.deserialize_stdlib_require library + if cached_pins + @pins = cached_pins @resolved = true + logger.warn { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } + cached_pins else - super - return unless resolved? - Cache.save('stdlib', "#{library}.ser", pins) + generated_pins = load_environment_to_pins(loader) + unless resolved? + logger.warn { "Could not resolve #{library.inspect}" } + return [] + end + PinCache.serialize_stdlib_require library, generated_pins + @pins = generated_pins end end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 7cce32292..8a64f3700 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -90,19 +90,20 @@ def config(directory = '.') # @return [void] def clear puts "Deleting all cached documentation (gems, core and stdlib)" - Solargraph::Cache.clear + Solargraph::PinCache.clear end map 'clear-cache' => :clear map 'clear-cores' => :clear desc 'cache', 'Cache a gem', hide: true + option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false # @return [void] # @param gem [String] # @param version [String, nil] def cache gem, version = nil + api_map = Solargraph::ApiMap.load(Dir.pwd) spec = Gem::Specification.find_by_name(gem, version) - pins = GemPins.build(spec) - Cache.save('gems', "#{spec.name}-#{spec.version}.ser", pins) + api_map.do_cache(spec, version, out: $stdout) end desc 'uncache GEM [...GEM]', "Delete specific cached gem documentation" @@ -117,18 +118,17 @@ def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? gems.each do |gem| if gem == 'core' - Cache.uncache("core.ser") + PinCache.uncache_core next end if gem == 'stdlib' - Cache.uncache("stdlib") + PinCache.uncache_stdlib next end spec = Gem::Specification.find_by_name(gem) - Cache.uncache('gems', "#{spec.name}-#{spec.version}.ser") - Cache.uncache('gems', "#{spec.name}-#{spec.version}.yardoc") + PinCache.uncache_gem(spec, out: $stdout) end end @@ -256,14 +256,8 @@ def pin_description pin # @param gemspec [Gem::Specification] # @return [void] def do_cache gemspec - cached = Yardoc.cached?(gemspec) - if cached && !options.rebuild - puts "Cache already exists for #{gemspec.name} #{gemspec.version}" - else - puts "#{cached ? 'Rebuilding' : 'Caching'} gem documentation for #{gemspec.name} #{gemspec.version}" - pins = GemPins.build(gemspec) - Cache.save('gems', "#{gemspec.name}-#{gemspec.version}.ser", pins) - end + api_map = ApiMap.load('.') + api_map.cache_gem(gemspec, options.rebuild, out: $stdout) end end end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 7d8fd73e3..dbe9588e1 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -133,6 +133,15 @@ def rbs_collection_path @gem_rbs_collection ||= read_rbs_collection_path end + def rbs_collection_config_path + @rbs_collection_config_path ||= begin + unless directory.empty? || directory == '*' + yaml_file = File.join(directory, 'rbs_collection.yaml') + yaml_file if File.file?(yaml_file) + end + end + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] @@ -230,10 +239,9 @@ def require_plugins # @return [String, nil] def read_rbs_collection_path - yaml_file = File.join(directory, 'rbs_collection.yaml') - return unless File.file?(yaml_file) + return unless rbs_collection_config_path - path = YAML.load_file(yaml_file)&.fetch('path') + path = YAML.load_file(rbs_collection_config_path)&.fetch('path') # make fully qualified File.expand_path(path, directory) end diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 4fd9b193f..797413230 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -12,7 +12,7 @@ module Yardoc # @param gemspec [Gem::Specification] # @return [String] The path to the cached yardoc. def cache(gemspec) - path = path_for(gemspec) + path = PinCache.yardoc_path gemspec return path if cached?(gemspec) Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" @@ -26,18 +26,10 @@ def cache(gemspec) # # @param gemspec [Gem::Specification] def cached?(gemspec) - yardoc = File.join(path_for(gemspec), 'complete') + yardoc = File.join(PinCache.yardoc_path(gemspec), 'complete') File.exist?(yardoc) end - # Get the absolute path for a cached gem yardoc. - # - # @param gemspec [Gem::Specification] - # @return [String] - def path_for(gemspec) - File.join(Solargraph::Cache.base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc") - end - # Load a gem's yardoc and return its code objects. # # @note This method modifies the global YARD registry. @@ -45,7 +37,7 @@ def path_for(gemspec) # @param gemspec [Gem::Specification] # @return [Array] def load!(gemspec) - YARD::Registry.load! path_for(gemspec) + YARD::Registry.load! PinCache.yardoc_path gemspec YARD::Registry.all end end diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index a72444e14..9a519a902 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -4,12 +4,13 @@ before :all do # We use ast here because it's a known dependency. gemspec = Gem::Specification.find_by_name('ast') - pins = Solargraph::GemPins.build(gemspec) - Solargraph::Cache.save('gems', "#{gemspec.name}-#{gemspec.version}.ser", pins) + yard_pins = Solargraph::GemPins.build_yard_pins(gemspec) + Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) end it 'generates pins from gems' do doc_map = Solargraph::DocMap.new(['ast'], []) + doc_map.cache_all!($stderr) node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } expect(node_pin).to be_a(Solargraph::Pin::Namespace) end @@ -26,7 +27,8 @@ end allow(Gem::Specification).to receive(:find_by_path).with('not_a_gem').and_return(gemspec) doc_map = Solargraph::DocMap.new(['not_a_gem'], [gemspec]) - expect(doc_map.uncached_gemspecs).to eq([gemspec]) + expect(doc_map.uncached_yard_gemspecs).to eq([gemspec]) + expect(doc_map.uncached_rbs_collection_gemspecs).to eq([gemspec]) end it 'imports all gems when bundler/require used' do diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index e3906282a..94044c52a 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true describe Solargraph::GemPins do - it 'merges YARD and RBS' do - spec = Gem::Specification.find_by_name('rbs') - pins = Solargraph::GemPins.build(spec) + it 'can merge YARD and RBS' do + gemspec = Gem::Specification.find_by_name('rbs') + yard_pins = Solargraph::GemPins.build_yard_pins(gemspec) + rbs_map = Solargraph::RbsMap.from_gemspec(gemspec, nil, nil) + pins = Solargraph::GemPins.combine yard_pins, rbs_map.pins + core_root = pins.find { |pin| pin.path == 'RBS::EnvironmentLoader#core_root' } expect(core_root.return_type.to_s).to eq('Pathname, nil') expect(core_root.location.filename).to end_with('environment_loader.rb') diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index b1b5ce27d..b06c975d1 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -1,14 +1,14 @@ describe Solargraph::RbsMap do it 'loads from a gemspec' do spec = Gem::Specification.find_by_name('rbs') - rbs_map = Solargraph::RbsMap.from_gemspec(spec) + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader#add_collection') expect(pin).to be end it 'converts constants and aliases to correct types' do spec = Gem::Specification.find_by_name('rbs') - rbs_map = Solargraph::RbsMap.from_gemspec(spec) + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader::DEFAULT_CORE_ROOT') expect(pin.return_type.tag).to eq('Pathname') pin = rbs_map.path_pin('RBS::EnvironmentWalker::InstanceNode') diff --git a/spec/type_checker/levels/normal_spec.rb b/spec/type_checker/levels/normal_spec.rb index e63ec25d8..62970fbd9 100644 --- a/spec/type_checker/levels/normal_spec.rb +++ b/spec/type_checker/levels/normal_spec.rb @@ -222,8 +222,9 @@ def bar; end # lack typed methods. A better test wouldn't depend on the state of # vendored code. gemspec = Gem::Specification.find_by_name('kramdown-parser-gfm') - pins = Solargraph::GemPins.build(gemspec) - Solargraph::Cache.save('gems', "#{gemspec.name}-#{gemspec.version}.ser", pins) + yard_pins = Solargraph::GemPins.build_yard_pins(gemspec) + Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + checker = type_checker(%( require 'kramdown-parser-gfm' # @type [String] diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 8765e67b6..5d98c9218 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -55,14 +55,11 @@ def bar(a); end # @todo This test uses kramdown-parser-gfm because it's a gem dependency known to # lack typed methods. A better test wouldn't depend on the state of # vendored code. - gemspec = Gem::Specification.find_by_name('kramdown-parser-gfm') - pins = Solargraph::GemPins.build(gemspec) - Solargraph::Cache.save('gems', "#{gemspec.name}-#{gemspec.version}.ser", pins) source_map = Solargraph::SourceMap.load_string(%( require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.new + api_map = Solargraph::ApiMap.load_with_cache('.') api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) checker = Solargraph::TypeChecker.new('test.rb', api_map: api_map, level: :strict) expect(checker.problems).to be_empty From 8c311a8915cd01c8ef2d7154acf0d061c756633a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:19:07 -0400 Subject: [PATCH 052/116] Add safety check --- lib/solargraph/api_map/store.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 5028f7b3f..d219abf65 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -204,6 +204,7 @@ def catalog pinsets @pinsets = pinsets @indexes = [] pinsets.each do |pins| + raise ArgumentError, "Pinset must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) if @indexes.last && pins.empty? @indexes.push @indexes.last else From 7ec55ba5ec5839541ab216b5b9b903b4cb0ca4b4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:24:36 -0400 Subject: [PATCH 053/116] Add safety check --- lib/solargraph/api_map.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 029260419..36a4fbfec 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -27,6 +27,7 @@ def initialize pins: [] @source_map_hash = {} @cache = Cache.new @method_alias_stack = [] + raise ArgumentError, "pins must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) index pins end @@ -61,6 +62,7 @@ def inspect # @param pins [Array] # @return [self] def index pins + raise ArgumentError, "pins must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) # @todo This implementation is incomplete. It should probably create a # Bench. @source_map_hash = {} From 4916a3826a393b7ed804e2696f9fded2ff863cd7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:32:35 -0400 Subject: [PATCH 054/116] Better logging / safety checks --- lib/solargraph/api_map.rb | 1 + lib/solargraph/doc_map.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 36a4fbfec..4700d7c9b 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -68,6 +68,7 @@ def index pins @source_map_hash = {} implicit.clear cache.clear + raise ArgumentError, "@@core_map.pins must be an Enumerable - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.is_a?(Enumerable) store.update @@core_map.pins, pins self end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 7116776ab..7db58b251 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -46,8 +46,11 @@ def initialize(requires, preferences, rbs_collection_path = nil, rbs_collection_ end def cache_all!(out) - gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') - out.puts "Caching pins for gems: #{gem_desc.inspect}" unless uncached_gemspecs.empty? + # if we log at debug level: + if logger.info? + gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') + logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty? + end logger.warn { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } logger.warn { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } load_serialized_gem_pins @@ -63,7 +66,7 @@ def cache_all!(out) def cache_yard_pins(gemspec, out) pins = GemPins.build_yard_pins(gemspec) PinCache.serialize_yard_gem(gemspec, pins) - out.puts "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" unless pins.empty? + logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? end def cache_rbs_collection_pins(gemspec, out) @@ -73,7 +76,7 @@ def cache_rbs_collection_pins(gemspec, out) # cache pins even if result is zero, so we don't retry building pins pins ||= [] PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins) - out.puts "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? + logger.info { "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? } end # @param gemspec [Gem::Specification] @@ -212,7 +215,7 @@ def deserialize_combined_pin_cache(gemspec) combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins return combined_pins_in_memory[[gemspec.name, gemspec.version]] else - logger.warn { "No pins found for gem #{gemspec.name}:#{gemspec.version}" } + logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" } return nil end end From d36ff69ef603405e0dc5dbcda7ebbcc13f8628fc Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:39:29 -0400 Subject: [PATCH 055/116] Better logging / safety checks --- lib/solargraph/api_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 4700d7c9b..8f12926c6 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -68,7 +68,7 @@ def index pins @source_map_hash = {} implicit.clear cache.clear - raise ArgumentError, "@@core_map.pins must be an Enumerable - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.is_a?(Enumerable) + raise ArgumentError, "@@core_map.pins must be an Enumerable - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.class.include?(Enumerable) store.update @@core_map.pins, pins self end From 65084563f53000724e5b45e273a1d9072120231e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:42:24 -0400 Subject: [PATCH 056/116] Better logging / safety checks --- lib/solargraph/api_map.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 8f12926c6..8812c0c05 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -62,13 +62,13 @@ def inspect # @param pins [Array] # @return [self] def index pins - raise ArgumentError, "pins must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) + raise ArgumentError, "pins must be an Array - was #{pins.class} (#{pins})}" unless pins.is_a?(Array) # @todo This implementation is incomplete. It should probably create a # Bench. @source_map_hash = {} implicit.clear cache.clear - raise ArgumentError, "@@core_map.pins must be an Enumerable - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.class.include?(Enumerable) + raise ArgumentError, "@@core_map.pins must be an Array - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.is_a?(Array) store.update @@core_map.pins, pins self end From 44635464bfb8c2d1bc218d99e4a4e5ef8e8d4d6d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:44:25 -0400 Subject: [PATCH 057/116] Better logging / safety checks --- lib/solargraph/api_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 8812c0c05..b06e09a9a 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -68,7 +68,7 @@ def index pins @source_map_hash = {} implicit.clear cache.clear - raise ArgumentError, "@@core_map.pins must be an Array - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.is_a?(Array) + raise ArgumentError, "@@core_map.pins must be an Array - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.class == Array store.update @@core_map.pins, pins self end From a97cd9985b6e3aacafa67cb84cf02a095f1aac09 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:47:34 -0400 Subject: [PATCH 058/116] Better logging / safety checks --- lib/solargraph/rbs_map/core_map.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 8cb70adc0..beddf872e 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -31,6 +31,7 @@ def pins PinCache.serialize_core @pins end + @pins end def loader From 5b1fc17afce6206dcb975dea5a224be31ca35b6e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 05:49:56 -0400 Subject: [PATCH 059/116] Adjust log levels --- lib/solargraph/doc_map.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 7db58b251..a6d850c74 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -51,8 +51,8 @@ def cache_all!(out) gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty? end - logger.warn { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } - logger.warn { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } + logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" } + logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } load_serialized_gem_pins uncached_gemspecs.each do |gemspec| out.puts "Caching pins for gem #{gemspec.name}:#{gemspec.version}" From a56cf1f75e1c13d5f4064190b41e116c4f83b09c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:03:47 -0400 Subject: [PATCH 060/116] Handle un-installed RBS collection case --- lib/solargraph/rbs_map.rb | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index f6f9bd047..5732af909 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -46,15 +46,10 @@ def loader # if the config for where information comes form changes. def cache_key @hextdigest ||= begin - data_arr = rbs_collection_paths.map do |dir| - # TODO can I get this more directly upstream? - # rbs_collection_config_path = Pathname.new(dir).join("..", "rbs_collection.yaml") - collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) - gem_config = collection_config.gem(library) - next unless gem_config - gem_config.to_s - end.sort.compact - if data_arr.empty? + collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) + gem_config = collection_config.gem(library) + data = gem_config&.to_s + if data.nil? || data.empty? if resolved? # definitely came from the gem itself and not elsewhere - # only one version per gem @@ -77,7 +72,7 @@ def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path # try any version of the gem in the collection RbsMap.new(gemspec.name, nil, rbs_collection_paths: [rbs_collection_path].compact, - rbs_collection_config_path: rbs_collection_config_path) + rbs_collection_config_path: rbs_collection_config_path) end # @generic T @@ -101,7 +96,10 @@ def resolved? def repository @repository ||= RBS::Repository.new(no_stdlib: false).tap do |repo| - @rbs_collection_paths.each { |dir| repo.add(Pathname.new(dir)) } + @rbs_collection_paths.each do |dir| + dir_path = Pathname.new(dir) + repo.add(dir_path) if dir_path.exist? && dir_path.directory? + end end end From 277aa3da3ac2cb8611072110d91f31e5fe058757 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:05:22 -0400 Subject: [PATCH 061/116] Handle un-installed RBS collection case --- lib/solargraph/rbs_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 5732af909..da06646f8 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -58,7 +58,7 @@ def cache_key 'unresolved' end else - Digest::SHA1.hexdigest(data_arr.join) + Digest::SHA1.hexdigest(data) end end end From 419ae6d76df0054ba509404fc322b6049f46a535 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:11:02 -0400 Subject: [PATCH 062/116] Handle un-installed RBS collection case --- lib/solargraph/rbs_map.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index da06646f8..3fbc93c58 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -46,9 +46,12 @@ def loader # if the config for where information comes form changes. def cache_key @hextdigest ||= begin - collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) - gem_config = collection_config.gem(library) - data = gem_config&.to_s + data = nil + if rbs_collection_config_path + collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) + gem_config = collection_config.gem(library) + data = gem_config&.to_s + end if data.nil? || data.empty? if resolved? # definitely came from the gem itself and not elsewhere - From 55e2c02b29a96da77d2d880a862ad5442b7190ea Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:20:10 -0400 Subject: [PATCH 063/116] Handle un-installed RBS collection case --- lib/solargraph/rbs_map.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 3fbc93c58..0b4e91a28 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -48,9 +48,12 @@ def cache_key @hextdigest ||= begin data = nil if rbs_collection_config_path - collection_config = RBS::Collection::Config.from_path RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) - gem_config = collection_config.gem(library) - data = gem_config&.to_s + lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) + if lockfile_path.exist? + collection_config = RBS::Collection::Config.from_path lockfile_path + gem_config = collection_config.gem(library) + data = gem_config&.to_s + end end if data.nil? || data.empty? if resolved? From 84cee31229357afc8ec5dd96d12425e22cc5b0e5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:28:14 -0400 Subject: [PATCH 064/116] Fix type issue --- lib/solargraph/api_map.rb | 2 +- spec/type_checker/levels/strict_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index b06e09a9a..df4c34e06 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -209,7 +209,7 @@ class << self # @param directory [String] # @param out [IO] The output stream for messages # @return [ApiMap] - def self.load_with_cache directory, out = IO::NULL + def self.load_with_cache directory, out api_map = load(directory) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 5d98c9218..5c51d2779 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -59,7 +59,7 @@ def bar(a); end require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.load_with_cache('.') + api_map = Solargraph::ApiMap.load_with_cache('.', $stdout) api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) checker = Solargraph::TypeChecker.new('test.rb', api_map: api_map, level: :strict) expect(checker.problems).to be_empty From c2b4138a1ee050b5d80d7141996032e0e9d2858a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 11 Jun 2025 06:57:22 -0400 Subject: [PATCH 065/116] Drop excess checks --- lib/solargraph/api_map.rb | 3 --- lib/solargraph/api_map/store.rb | 1 - 2 files changed, 4 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index df4c34e06..56374d999 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -27,7 +27,6 @@ def initialize pins: [] @source_map_hash = {} @cache = Cache.new @method_alias_stack = [] - raise ArgumentError, "pins must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) index pins end @@ -62,13 +61,11 @@ def inspect # @param pins [Array] # @return [self] def index pins - raise ArgumentError, "pins must be an Array - was #{pins.class} (#{pins})}" unless pins.is_a?(Array) # @todo This implementation is incomplete. It should probably create a # Bench. @source_map_hash = {} implicit.clear cache.clear - raise ArgumentError, "@@core_map.pins must be an Array - was #{@@core_map.pins.class} (#{@@core_map.pins})}" unless @@core_map.pins.class == Array store.update @@core_map.pins, pins self end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index d219abf65..5028f7b3f 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -204,7 +204,6 @@ def catalog pinsets @pinsets = pinsets @indexes = [] pinsets.each do |pins| - raise ArgumentError, "Pinset must be an Enumerable - was #{pins.class} (#{pins})}" unless pins.is_a?(Enumerable) if @indexes.last && pins.empty? @indexes.push @indexes.last else From d633225878f44e94abba722cc2f352c722f1e5e9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 08:24:19 -0400 Subject: [PATCH 066/116] Bugfixes --- lib/solargraph/pin_cache.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 5721b4236..2fc564d49 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -43,7 +43,7 @@ def serialize_stdlib_require require, pins end def core_path - 'core.ser' + File.join(work_dir, 'core.ser') end def deserialize_core @@ -108,7 +108,7 @@ def uncache_stdlib def uncache_gem(gemspec, hash, out: nil) uncache(yardoc_path(gemspec), out: out) - uncache(rbs_collection(gemspec, hash), out: out) + uncache(rbs_collection_path(gemspec, hash), out: out) uncache(yard_gem_path(gemspec), out: out) end From 02c7c4ebb491af1fe8b0ca49deca17473fbcb1eb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 14:50:43 -0400 Subject: [PATCH 067/116] Ensure that pin locations are always populated --- .github/workflows/typecheck.yml | 2 +- lib/solargraph.rb | 19 +++++++++++++ lib/solargraph/pin/base.rb | 7 +++++ lib/solargraph/rbs_map/conversions.rb | 7 +++-- lib/solargraph/yard_map/helpers.rb | 28 ++++++++++++++++++- lib/solargraph/yard_map/mapper/to_constant.rb | 5 +--- lib/solargraph/yard_map/mapper/to_method.rb | 7 +---- .../yard_map/mapper/to_namespace.rb | 9 ++---- 8 files changed, 64 insertions(+), 20 deletions(-) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 5b1b5e151..9c827049f 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -31,4 +31,4 @@ jobs: - name: Install gems run: bundle install - name: Typecheck self - run: bundle exec solargraph typecheck --level typed + run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 352b0eaad..8834fc3fb 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -52,6 +52,25 @@ class InvalidRubocopVersionError < RuntimeError; end dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') + # @param type [Symbol] Type of assert. Not used yet, but may be + # used in the future to allow configurable asserts mixes for + # different situations. + def self.asserts_on?(type) + if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? + false + elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' + true + else + logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" + false + end + end + + def self.assert_or_log(type, msg = nil, &block) + raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type) + logger.info msg, &block + end + # A convenience method for Solargraph::Logging.logger. # # @return [Logger] diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 1e79397a7..90571510d 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -44,6 +44,13 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @source = source @comments = comments @source = source + assert_location_provided + 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 # @return [String] diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 0aca728f1..cd63dc77e 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -267,6 +267,7 @@ def global_decl_to_pin decl name: name, closure: closure, comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location) ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) @@ -452,7 +453,8 @@ def cvar_to_pin(decl, closure) pin = Solargraph::Pin::ClassVariable.new( name: name, closure: closure, - comments: decl.comment&.string + comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location) ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) @@ -467,7 +469,8 @@ def civar_to_pin(decl, closure) pin = Solargraph::Pin::InstanceVariable.new( name: name, closure: closure, - comments: decl.comment&.string + comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location) ) rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) 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 46c585264..6cbefc455 100644 --- a/lib/solargraph/yard_map/mapper/to_constant.rb +++ b/lib/solargraph/yard_map/mapper/to_constant.rb @@ -8,10 +8,7 @@ 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] - ) + 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 58c040573..21435ee6a 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -14,12 +14,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 62425d0f1..9539242e3 100644 --- a/lib/solargraph/yard_map/mapper/to_namespace.rb +++ b/lib/solargraph/yard_map/mapper/to_namespace.rb @@ -8,13 +8,10 @@ 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] - ) + 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, From 0bbed17d1c2d278e12dcb45cf9e653808c867390 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 15:45:27 -0400 Subject: [PATCH 068/116] Drop ill-conceived backup location info --- lib/solargraph/pin/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index d1f035dc8..9be9e7ab4 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -58,7 +58,7 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam def combine_with(other, attrs={}) raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class type_location = choose(other, :type_location) - location = choose(other, :location) || type_location + location = choose(other, :location) combined_name = combine_name(other) new_attrs = { location: location, From 9e413facbc64063bd3ce860611e65378e233e212 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 15:53:02 -0400 Subject: [PATCH 069/116] Fix CLI cache/uncache issues --- lib/solargraph/pin_cache.rb | 27 ++++++++++++++++++++++----- lib/solargraph/shell.rb | 2 +- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 2fc564d49..774fba90f 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -74,6 +74,10 @@ def rbs_collection_path(gemspec, hash) File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") end + def rbs_collection_path_prefix(gemspec) + File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") + end + def deserialize_rbs_collection_gem(gemspec, hash) load(rbs_collection_path(gemspec, hash)) end @@ -106,9 +110,9 @@ def uncache_stdlib uncache("stdlib") end - def uncache_gem(gemspec, hash, out: nil) + def uncache_gem(gemspec, out: nil) uncache(yardoc_path(gemspec), out: out) - uncache(rbs_collection_path(gemspec, hash), out: out) + uncache_by_prefix(rbs_collection_path_prefix(gemspec), out: out) uncache(yard_gem_path(gemspec), out: out) end @@ -146,14 +150,27 @@ def save file, pins end # @return [void] - # @param path [Array] - def uncache *path, out: nil - path = File.join(work_dir, *path) + # @param path_segments [Array] + def uncache *path_segments, out: nil + path = File.join(*path_segments) if File.exist?(path) FileUtils.rm_rf path, secure: true out.puts "Clearing pin cache in #{path}" unless out.nil? end end + + # @return [void] + # @param path_segments [Array] + def uncache_by_prefix *path_segments, out: nil + path = File.join(*path_segments) + glob = "#{path}*" + out.puts "Clearing pin cache in #{glob}" unless out.nil? + Dir.glob(glob).each do |file| + next unless File.file?(file) + FileUtils.rm_rf file, secure: true + out.puts "Clearing pin cache in #{file}" unless out.nil? + end + end end end end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 8a64f3700..5b0588b7a 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -103,7 +103,7 @@ def clear def cache gem, version = nil api_map = Solargraph::ApiMap.load(Dir.pwd) spec = Gem::Specification.find_by_name(gem, version) - api_map.do_cache(spec, version, out: $stdout) + api_map.cache_gem(spec, rebuild: options[:rebuild], out: $stdout) end desc 'uncache GEM [...GEM]', "Delete specific cached gem documentation" From f3553e7441ae7928d2d5ed289f95741fb07a7e3e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 19:36:43 -0400 Subject: [PATCH 070/116] Add more to dodgy_visibility_source? --- lib/solargraph/pin/method.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 6792b9b48..17747a863 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -441,7 +441,9 @@ def dodgy_visibility_source? # as of 2025-03-12, the RBS generator used for # e.g. activesupport did not understand 'private' markings # inside 'class << self' blocks, but YARD did OK at it - source == :rbs && scope == :class && type_location&.filename&.include?('generated') || + source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_value.undefined? || + # YARD's RBS generator seems to miss a lot of should-be protected instance methods + source == :rbs && scope == :instance && namespace.start_with?('YARD::') || # private on attr_readers seems to be broken in Prism's auto-generator script source == :rbs && scope == :instance && namespace.start_with?('Prism::') || # The RBS for the RBS gem itself seems to use private as a From f15c23da8447cb42117d2e99d5f2d71f479b34ca Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 12 Jun 2025 19:39:48 -0400 Subject: [PATCH 071/116] Also clear combined pins in uncache command --- lib/solargraph/pin_cache.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 774fba90f..849adcaf1 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -90,6 +90,10 @@ def combined_path(gemspec, hash) File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser") end + def combined_path_prefix(gemspec) + File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-") + end + def serialize_combined_gem(gemspec, hash, pins) save(combined_path(gemspec, hash), pins) end @@ -114,6 +118,7 @@ def uncache_gem(gemspec, out: nil) uncache(yardoc_path(gemspec), out: out) uncache_by_prefix(rbs_collection_path_prefix(gemspec), out: out) uncache(yard_gem_path(gemspec), out: out) + uncache_by_prefix(combined_path_prefix(gemspec), out: out) end # @return [void] From 8ab3f13bf143e035afe314f85eb78fe2ff5f5ad5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 14 Jun 2025 12:31:58 -0400 Subject: [PATCH 072/116] Comply with YARD documentation on Hash tag format Fixes #961 See https://www.rubydoc.info/gems/yard/file/docs/Tags.md#hashes --- lib/solargraph/complex_type/type_methods.rb | 6 +++++- lib/solargraph/complex_type/unique_type.rb | 7 +++++++ spec/complex_type_spec.rb | 14 +++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index 27826b60a..de2115a0f 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -171,7 +171,11 @@ def generate_substring_from(&to_str) elsif fixed_parameters? "(#{subtypes_str})" else - "<#{subtypes_str}>" + if name == 'Hash' + "<#{key_types_str}, #{subtypes_str}>" + else + "<#{key_types_str}#{subtypes_str}>" + end end end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 68c4edd5f..799d6525c 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -55,6 +55,13 @@ def self.parse name, substring = '', make_rooted: nil key_types.concat(subs[0].map { |u| ComplexType.new([u]) }) # @sg-ignore subtypes.concat(subs[1].map { |u| ComplexType.new([u]) }) + elsif parameters_type == :list && name == 'Hash' + # Treat Hash as Hash{A => B} + if subs.length != 2 + raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring} - must have exactly two parameters" + end + key_types.concat(subs[0].map { |u| ComplexType.new([u]) }) + subtypes.concat(subs[1].map { |u| ComplexType.new([u]) }) else subtypes.concat subs end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 1edc2950a..ea1050cc7 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -35,7 +35,7 @@ end it "parses multiple subtypes" do - types = Solargraph::ComplexType.parse 'Hash' + types = Solargraph::ComplexType.parse 'Array' expect(types.length).to eq(1) expect(types.first.tag).to eq('Hash') expect(types.first.name).to eq('Hash') @@ -106,6 +106,18 @@ end end + it "parses Hash using <> notation" do + types = Solargraph::ComplexType.parse 'Hash' + expect(types.length).to eq(1) + expect(types.first.tag).to eq('Hash') + expect(types.first.name).to eq('Hash') + expect(types.first.key_types.length).to eq(1) + expect(types.first.key_types[0].name).to eq('Symbol') + expect(types.first.subtypes.length).to eq(1) + expect(types.first.subtypes[0].name).to eq('String') + expect(types.to_rbs).to eq('Hash[Symbol, String]') + end + it "identifies parametrized types" do types = Solargraph::ComplexType.parse('Array, Hash{String => Symbol}, Array(String, Integer)') expect(types.all?(&:parameters?)).to be(true) From da3ca5cd94e45563ea7f81c9bed43d7afd94631d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 14 Jun 2025 13:06:57 -0400 Subject: [PATCH 073/116] Adjust spec --- spec/complex_type_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index ea1050cc7..6bc8c5ffa 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -37,12 +37,12 @@ it "parses multiple subtypes" do types = Solargraph::ComplexType.parse 'Array' expect(types.length).to eq(1) - expect(types.first.tag).to eq('Hash') - expect(types.first.name).to eq('Hash') + expect(types.first.tag).to eq('Array') + expect(types.first.name).to eq('Array') expect(types.first.subtypes.length).to eq(2) expect(types.first.subtypes[0].name).to eq('Symbol') expect(types.first.subtypes[1].name).to eq('String') - expect(types.to_rbs).to eq('Hash[Symbol, String]') + expect(types.to_rbs).to eq('Array[Symbol, String]') end it "detects namespace and scope for simple types" do From 75ff83d212a1fc1bf4800c665f5fde8f22688c0c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 14 Jun 2025 15:27:16 -0400 Subject: [PATCH 074/116] Restructure ComplexType specs towards YARD doc compliance --- spec/complex_type_spec.rb | 1124 ++++++++++++++++++++++--------------- 1 file changed, 663 insertions(+), 461 deletions(-) diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 1edc2950a..ee9927f99 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -1,535 +1,737 @@ -describe Solargraph::ComplexType do - it "parses a simple type" do - types = Solargraph::ComplexType.parse 'String' - expect(types.length).to eq(1) - expect(types.first.tag).to eq('String') - expect(types.first.name).to eq('String') - expect(types.first.subtypes).to be_empty - expect(types.first.to_rbs).to eq('String') - end +describe 'YARD type specifier list parsing' do + context 'in compliance with https://www.rubydoc.info/gems/yard/file/docs/Tags.md#type-list-conventions' do + # Types Specifier List + # + # In some cases, a tag will allow for a "types specifier list"; this + # will be evident from the use of the [Types] syntax in the tag + # signature. A types specifier list is a comma separated list of + # types, most often classes or modules, but occasionally + # literals. + # + it 'parses zero types as separate arguments' do + types = Solargraph::ComplexType.parse + expect(types.length).to eq(0) + end - it "parses multiple types" do - types = Solargraph::ComplexType.parse 'String', 'Integer' - expect(types.length).to eq(2) - expect(types[0].tag).to eq('String') - expect(types[1].tag).to eq('Integer') - expect(types.to_rbs).to eq('(String | Integer)') - end + xit 'parses zero types as a string' do + types = Solargraph::ComplexType.parse '' + expect(types.length).to eq(0) + end - it "parses multiple types in a string" do - types = Solargraph::ComplexType.parse 'String, Integer' - expect(types.length).to eq(2) - expect(types[0].tag).to eq('String') - expect(types[1].tag).to eq('Integer') - expect(types.to_rbs).to eq('(String | Integer)') - end + it 'parses a single type' do + types = Solargraph::ComplexType.parse 'String' + expect(types.length).to eq(1) + expect(types.first.tag).to eq('String') + expect(types.first.name).to eq('String') + expect(types.first.subtypes).to be_empty + expect(types.first.to_rbs).to eq('String') + end - it "parses a subtype" do - types = Solargraph::ComplexType.parse 'Array' - expect(types.length).to eq(1) - expect(types.first.tag).to eq('Array') - expect(types.first.name).to eq('Array') - expect(types.first.subtypes.length).to eq(1) - expect(types.first.subtypes.first.name).to eq('String') - expect(types.to_rbs).to eq('Array[String]') - end + it 'parses multiple types as separate arguments' do + types = Solargraph::ComplexType.parse 'String', 'Integer' + expect(types.length).to eq(2) + expect(types[0].tag).to eq('String') + expect(types[1].tag).to eq('Integer') + expect(types.to_rbs).to eq('(String | Integer)') + end - it "parses multiple subtypes" do - types = Solargraph::ComplexType.parse 'Hash' - expect(types.length).to eq(1) - expect(types.first.tag).to eq('Hash') - expect(types.first.name).to eq('Hash') - expect(types.first.subtypes.length).to eq(2) - expect(types.first.subtypes[0].name).to eq('Symbol') - expect(types.first.subtypes[1].name).to eq('String') - expect(types.to_rbs).to eq('Hash[Symbol, String]') - end + it 'parses multiple types in a string' do + types = Solargraph::ComplexType.parse 'String, Integer' + expect(types.length).to eq(2) + expect(types[0].tag).to eq('String') + expect(types[1].tag).to eq('Integer') + expect(types.to_rbs).to eq('(String | Integer)') + end - it "detects namespace and scope for simple types" do - types = Solargraph::ComplexType.parse 'Class' - expect(types.length).to eq(1) - expect(types.first.namespace).to eq('Class') - expect(types.first.scope).to eq(:instance) - expect(types.to_rbs).to eq('Class') - end + # For example, the following @return tag lists a set of + # types returned by a method: + # + # # Finds an object or list of objects in the db using a query + # # @return [String, Array, nil] the object or objects to + # # find in the database. Can be nil. + # def find(query) finder_code_here end + it 'parses class, generic class and literal in a string' do + types = Solargraph::ComplexType.parse 'String, Array, nil' + expect(types.length).to eq(3) + expect(types[0].tag).to eq('String') + expect(types[1].tag).to eq('Array') + expect(types[2].tag).to eq('nil') + expect(types.to_rbs).to eq('(String | Array[String] | nil)') + end - it "identify rooted types" do - types = Solargraph::ComplexType.parse '::Array' - expect(types.map(&:rooted?)).to eq([true]) - expect(types.to_rbs).to eq('::Array') - end + # + # A list of conventions for type names is specified + # below. Typically, however, any Ruby literal or class/module is + # allowed here. + # + # + # Duck-types (method names prefixed with "#") are also + # allowed. + it 'parses duck types' do + types = Solargraph::ComplexType.parse('#method') + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('Object') + expect(types.first.scope).to eq(:instance) + expect(types.first.duck_type?).to be(true) + # RBS solves this problem with type-only signatures + expect(types.to_rbs).to eq('untyped') + end - it "identify unrooted types" do - types = Solargraph::ComplexType.parse 'Array' - expect(types.map(&:rooted?)).to eq([false]) - end + # + # Note that the type specifier list is always an optional field and + # can be omitted when present in a tag signature. This is the reason + # why it is surrounded by brackets. It is also a freeform list, and + # can contain any list of values, though a set of conventions for + # how to list types is described below. + # + # Type List Conventions + # + # A list of examples of common type listings and what they translate + # into is available at http://yardoc.org/types. + # + xit 'parses type examples from http://yardoc.org/types' + # + # Typically, a type list contains a list of classes or modules that + # are associated with the tag. In some cases, however, certain + # special values are allowed or required to be listed. This section + # discusses the syntax for specifying Ruby types inside of type + # specifier lists, as well as the other non-Ruby types that are + # accepted by convention in these lists. + # + # It's important to realize that the conventions listed here may not + # always adequately describe every type signature, and is not meant + # to be a complete syntax. This is why the types specifier list is + # freeform and can contain any set of values. The conventions + # defined here are only conventions, and if they do not work for + # your type specifications, you can define your own appropriate + # conventions. + # + # Note that a types specifier list might also be used for non-Type + # values. In this case, the tag documentation will describe what + # values are allowed within the type specifier list. + # + # Class or Module Types + # + # Any Ruby type is allowed as a class or module type. Such a type is + # simply the name of the class or module. + + # Note that one extra type that is accepted by convention is the + # Boolean type, which represents both the TrueClass and FalseClass + # types. This type does not exist in Ruby, however. + + it 'typifies Booleans' do + api_map = double(Solargraph::ApiMap, qualify: nil) + type = Solargraph::ComplexType.parse('::Boolean') + qualified = type.qualify(api_map) + expect(qualified.tag).to eq('Boolean') + expect(qualified.to_rbs).to eq('bool') + end - it "detects namespace and scope for classes with subtypes" do - types = Solargraph::ComplexType.parse 'Class' - expect(types.length).to eq(1) - expect(types.first.namespace).to eq('String') - expect(types.first.scope).to eq(:class) - # RBS doesn't support individual class types like this - expect(types.to_rbs).to eq('Class') - end + # Parametrized Types + # + # In addition to basic types (like String or Array), YARD + # conventions allow for a "generics" like syntax to specify + # container objects or other parametrized types. The syntax is + # Type. For instance, an Array might + # contain only String objects, in which case the type specification + # would be Array. + + it 'parses a subtype' do + types = Solargraph::ComplexType.parse 'Array' + expect(types.length).to eq(1) + expect(types.first.tag).to eq('Array') + expect(types.first.name).to eq('Array') + expect(types.first.subtypes.length).to eq(1) + expect(types.first.subtypes.first.name).to eq('String') + expect(types.to_rbs).to eq('Array[String]') + end - it "detects namespace and scope for modules with subtypes" do - types = Solargraph::ComplexType.parse 'Module' - expect(types.length).to eq(1) - expect(types.first.namespace).to eq('Foo') - expect(types.first.scope).to eq(:class) - expect(types.to_rbs).to eq('Module') - multiple_types = Solargraph::ComplexType.parse 'Module, Class, String, nil' - expect(multiple_types.length).to eq(4) - expect(multiple_types.namespaces).to eq(['Foo', 'Bar', 'String', 'NilClass']) - # RBS doesn't support individual module types like this - expect(multiple_types.to_rbs).to eq('(Module | Class | String | nil)') - end + # Multiple parametrized types can be listed, separated by commas. + + it 'parses multiple subtypes' do + types = Solargraph::ComplexType.parse 'Array' + expect(types.length).to eq(1) + expect(types.first.tag).to eq('Array') + expect(types.first.name).to eq('Array') + expect(types.first.subtypes.length).to eq(2) + expect(types.first.subtypes[0].name).to eq('Symbol') + expect(types.first.subtypes[1].name).to eq('String') + expect(types.to_rbs).to eq('Array[Symbol, String]') + end - it "identifies duck types" do - types = Solargraph::ComplexType.parse('#method') - expect(types.length).to eq(1) - expect(types.first.namespace).to eq('Object') - expect(types.first.scope).to eq(:instance) - expect(types.first.duck_type?).to be(true) - expect(types.to_rbs).to eq('untyped') - end - it "identifies nil types" do - %w[nil Nil NIL].each do |t| - types = Solargraph::ComplexType.parse(t) + # Note that parametrized types are typically not order-dependent, in + # other words, a list of parametrized types can occur in any order + # inside of a type. An array specified as Array can + # contain any amount of Strings or Fixnums, in any order. When the + # order matters, use "order-dependent lists", described below. + # + # Duck-Types + # + # Duck-types are allowed in type specifier lists, and are identified + # by method names beginning with the "#" prefix. Typically, + # duck-types are recommended for @param tags only, though they can + # be used in other tags if needed. The following example shows a + # method that takes a parameter of any type that responds to the + # "read" method: + # + # # Reads from any I/O object. + # # @param io [#read] the input object to read from + # def read(io) io.read end + + # + # Hashes + # + + # Hashes can be specified either via the parametrized type discussed + # above, in the form Hash, or using the hash + # specific syntax: Hash{KeyTypes=>ValueTypes}. + it 'parses Hash using hash rocket notation' do + types = Solargraph::ComplexType.parse('Hash{String => Integer}') expect(types.length).to eq(1) - expect(types.first.namespace).to eq('NilClass') - expect(types.first.scope).to eq(:instance) - expect(types.first.nil_type?).to be(true) - expect(types.to_rbs).to eq('nil') + expect(types.first.tag).to eq('Hash{String => Integer}') + expect(types.first.namespace).to eq('Hash') + expect(types.first.substring).to eq('{String => Integer}') + expect(types.first.key_types.map(&:name)).to eq(['String']) + expect(types.first.value_types.map(&:name)).to eq(['Integer']) + expect(types.to_rbs).to eq('Hash[String, Integer]') end - end - it "identifies parametrized types" do - types = Solargraph::ComplexType.parse('Array, Hash{String => Symbol}, Array(String, Integer)') - expect(types.all?(&:parameters?)).to be(true) - expect(types.to_rbs).to eq('(Array[String] | Hash[String, Symbol] | [String, Integer])') - end + xit 'parses Hash using <> notation' do + types = Solargraph::ComplexType.parse 'Hash' + expect(types.length).to eq(1) + expect(types.first.tag).to eq('Hash') + expect(types.first.name).to eq('Hash') + expect(types.first.key_types.length).to eq(1) + expect(types.first.key_types[0].name).to eq('Symbol') + expect(types.first.subtypes.length).to eq(1) + expect(types.first.subtypes[0].name).to eq('String') + expect(types.to_rbs).to eq('Hash[Symbol, String]') + end - it "identifies list parameters" do - types = Solargraph::ComplexType.parse('Array') - expect(types.first.list_parameters?).to be(true) - expect(types.to_rbs).to eq('Array[String, Symbol]') - end + # In the latter case, KeyTypes or ValueTypes can also be a list of + # types separated by commas." + it 'parses multiple key/value types in hash parameters' do + types = Solargraph::ComplexType.parse('Hash{String, Symbol => Integer, BigDecimal}') + expect(types.length).to eq(1) + type = types.first + expect(type.hash_parameters?).to eq(true) + expect(type.key_types.map(&:name)).to eq(%w[String Symbol]) + expect(type.value_types.map(&:name)).to eq(%w[Integer BigDecimal]) + expect(type.to_rbs).to eq('Hash[(String | Symbol), (Integer | BigDecimal)]') + end - it "identifies hash parameters" do - types = Solargraph::ComplexType.parse('Hash{String => Integer}') - expect(types.length).to eq(1) - expect(types.first.hash_parameters?).to be(true) - expect(types.first.tag).to eq('Hash{String => Integer}') - expect(types.first.namespace).to eq('Hash') - expect(types.first.substring).to eq('{String => Integer}') - expect(types.first.key_types.map(&:name)).to eq(['String']) - expect(types.first.value_types.map(&:name)).to eq(['Integer']) - expect(types.to_rbs).to eq('Hash[String, Integer]') - end + # + # Order-Dependent Lists + # + + # An order dependent list is a set of types surrounded by "()" and + # separated by commas. This list must contain exactly those types in + # exactly the order specified. For instance, an Array containing a + # String, Fixnum and Hash in that order (and having exactly those 3 + # elements) would be listed as: Array(String, Fixnum, Hash). + + it 'parses tuples of tuples' do + type = Solargraph::ComplexType.parse('Array(Array(String), String)') + expect(type.tag).to eq('Array(Array(String), String)') + expect(type.to_rbs).to eq('[[String], String]') + expect(type.to_s).to eq('Array(Array(String), String)') + end - it "identifies fixed parameters" do - types = Solargraph::ComplexType.parse('Array(String, Symbol)') - expect(types.first.fixed_parameters?).to be(true) - expect(types.first.subtypes.map(&:namespace)).to eq(['String', 'Symbol']) - # RBS doesn't use a type name for tuples, just the [] shorthand - expect(types.to_rbs).to eq('[String, Symbol]') - end + # Literals + # + # Some literals are accepted by virtue of being Ruby literals, but + # also by YARD conventions. Here is a non-exhaustive list of certain + # accepted literal values: + + # true, false, nil — used when a method returns these explicit + # literal values. Note that if your method returns both true or + # false, you should use the Boolean conventional type instead. + + it 'understands literal true' do + type = Solargraph::ComplexType.parse('true') + expect(type.tag).to eq('true') + expect(type.to_rbs).to eq('true') + expect(type.to_s).to eq('true') + end - it "raises ComplexTypeError for unmatched brackets" do - expect { - Solargraph::ComplexType.parse('Array>') - }.to raise_error(Solargraph::ComplexTypeError) - expect { - Solargraph::ComplexType.parse('Array{String}}') - }.to raise_error(Solargraph::ComplexTypeError) - expect { - Solargraph::ComplexType.parse('Array(String, Integer') - }.to raise_error(Solargraph::ComplexTypeError) - expect { - Solargraph::ComplexType.parse('Array(String, Integer))') - }.to raise_error(Solargraph::ComplexTypeError) - end + it 'understands literal false' do + type = Solargraph::ComplexType.parse('false') + expect(type.tag).to eq('false') + expect(type.to_rbs).to eq('false') + expect(type.to_s).to eq('false') + end - it "raises ComplexTypeError for hash parameters without key => value syntax" do - expect { - Solargraph::ComplexType.parse('Hash{Foo}') - }.to raise_error(Solargraph::ComplexTypeError) - expect { - Solargraph::ComplexType.parse('Hash{Foo, Bar}') - }.to raise_error(Solargraph::ComplexTypeError) - end + # See literal details at + # https://github.com/ruby/rbs/blob/master/docs/syntax.md and + # https://yardoc.org/types.html + xit 'understands literal strings with double quotes' do + type = Solargraph::ComplexType.parse('"foo"') + expect(type.tag).to eq('"foo"') + expect(type.to_rbs).to eq('"foo"') + expect(type.to_s).to eq('String') + end - it "parses multiple key/value types in hash parameters" do - types = Solargraph::ComplexType.parse("Hash{String, Symbol => Integer, BigDecimal}") - expect(types.length).to eq(1) - type = types.first - expect(type.hash_parameters?).to eq(true) - expect(type.key_types.map(&:name)).to eq(['String', 'Symbol']) - expect(type.value_types.map(&:name)).to eq(['Integer', 'BigDecimal']) - expect(type.to_rbs).to eq('Hash[(String | Symbol), (Integer | BigDecimal)]') - end + xit 'understands literal strings with single quotes' do + type = Solargraph::ComplexType.parse("'foo'") + expect(type.tag).to eq("'foo'") + expect(type.to_rbs).to eq("'foo'") + expect(type.to_s).to eq('String') + end - it "parses recursive subtypes" do - types = Solargraph::ComplexType.parse('Array Integer}>') - expect(types.length).to eq(1) - expect(types.first.namespace).to eq('Array') - expect(types.first.substring).to eq(' Integer}>') - expect(types.first.subtypes.length).to eq(1) - expect(types.first.subtypes.first.namespace).to eq('Hash') - expect(types.first.subtypes.first.substring).to eq('{String => Integer}') - expect(types.first.subtypes.first.key_types.map(&:namespace)).to eq(['String']) - expect(types.first.subtypes.first.value_types.map(&:namespace)).to eq(['Integer']) - expect(types.to_rbs).to eq('Array[Hash[String, Integer]]') - end + it 'understands literal symbols' do + type = Solargraph::ComplexType.parse(':foo') + expect(type.tag).to eq(':foo') + expect(type.to_rbs).to eq(':foo') + expect(type.to_s).to eq(':foo') + end - let (:foo_bar_api_map) { - api_map = Solargraph::ApiMap.new - source = Solargraph::Source.load_string(%( - module Foo - class Bar - # @return [Bar] - def make_bar - end - end - end - )) - api_map.map source - api_map - } - - it "qualifies types with list parameters" do - original = Solargraph::ComplexType.parse('Class').first - expect(original).not_to be_rooted - qualified = original.qualify(foo_bar_api_map, 'Foo') - expect(qualified.tag).to eq('Class') - expect(qualified.rooted_tag).to eq('::Class<::Foo::Bar>') - expect(qualified).to be_rooted - expect(qualified.to_rbs).to eq('::Class') - end + it 'understands literal integers' do + type = Solargraph::ComplexType.parse('123') + expect(type.tag).to eq('123') + expect(type.to_rbs).to eq('123') + expect(type.to_s).to eq('123') + end - it "qualifies types with fixed parameters" do - original = Solargraph::ComplexType.parse('Array(String, Bar)').first - expect(original.to_rbs).to eq('[String, Bar]') - qualified = original.qualify(foo_bar_api_map, 'Foo') - expect(qualified).to be_rooted - expect(qualified.tag).to eq('Array(String, Foo::Bar)') - expect(qualified.to_rbs).to eq('[::String, ::Foo::Bar]') - end + # + # self — has the same meaning as Ruby's "self" keyword in the + # context of parameters or return types. Recommended mostly for + # @return tags that are chainable. + # - it "qualifies types with hash parameters" do - original = Solargraph::ComplexType.parse('Hash{String => Bar}').first - qualified = original.qualify(foo_bar_api_map, 'Foo') - expect(qualified.tag).to eq('Hash{String => Foo::Bar}') - expect(qualified.to_rbs).to eq('::Hash[::String, ::Foo::Bar]') - end + it 'parses a complex subtype as a self type' do + type = Solargraph::ComplexType.parse('Array').self_to_type(Solargraph::ComplexType.parse('Foo')) + expect(type.tag).to eq('Array>') + expect(type.to_rbs).to eq('Array[Foo[String]]') + end - it "returns string representations of the entire type array" do - type = Solargraph::ComplexType.parse('String', 'Array') - expect(type.to_s).to eq('String, Array') - # we want this surrounded by () so that it can be composed with - # other types without worrying about operator precedence - expect(type.to_rbs).to eq('(String | Array[String])') - end + # void — indicates that the type for this tag is explicitly + # undefined. Mostly used to specify @return tags that do not care + # about their return value. Using a void return tag is recommended + # over no type, because it makes the documentation more explicit + # about what the user should expect. YARD will also add a note for + # the user if they have undefined return types, making things clear + # that they should not use the return value of such a method. + # + # Reference Tags + # + # + # Reference tag syntax applies only to meta-data tags, not directives. + # + # If a tag's data begins with (see OBJECT) it is considered a + # "reference tag". A reference tag literally copies the tag data by + # the given tag name from the specified OBJECT. For instance, a + # method may copy all @param tags from a given object using the + # reference tag syntax: + # + # # @param user [String] the username for the operation + # # @param host [String] the host that this user is associated with + # # @param time [Time] the time that this operation took place + # def clean(user, host, time = Time.now) end + # + # # @param (see #clean) + # def activate(user, host, time = Time.now) end + # + xit 'understands reference tags' + end + + context 'offers machine users error messages given non-sensical types' do + it 'raises ComplexTypeError for unmatched brackets' do + expect do + Solargraph::ComplexType.parse('Array>') + end.to raise_error(Solargraph::ComplexTypeError) + expect do + Solargraph::ComplexType.parse('Array{String}}') + end.to raise_error(Solargraph::ComplexTypeError) + expect do + Solargraph::ComplexType.parse('Array(String, Integer') + end.to raise_error(Solargraph::ComplexTypeError) + expect do + Solargraph::ComplexType.parse('Array(String, Integer))') + end.to raise_error(Solargraph::ComplexTypeError) + end - it "returns the first type when multiple were parsed with #tag" do - type = Solargraph::ComplexType.parse('String, Array') - expect(type.tag).to eq('String') - expect(type.to_rbs).to eq('(String | Array[String])') + it 'raises ComplexTypeError for hash parameters without key => value syntax' do + expect do + Solargraph::ComplexType.parse('Hash{Foo}') + end.to raise_error(Solargraph::ComplexTypeError) + expect do + Solargraph::ComplexType.parse('Hash{Foo, Bar}') + end.to raise_error(Solargraph::ComplexTypeError) + end end - it "raises NoMethodError for missing methods" do - type = Solargraph::ComplexType.parse('String') - expect { type.undefined_method }.to raise_error(NoMethodError) - end + context 'offers type queries orthogonal to YARD spec' do + context 'defines namespace concept which strips Class<> and Module<> from type' do + # + # Solargraph extensions and library features + # - it "typifies Booleans" do - api_map = double(Solargraph::ApiMap, qualify: nil) - type = Solargraph::ComplexType.parse('::Boolean') - qualified = type.qualify(api_map) - expect(qualified.tag).to eq('Boolean') - expect(qualified.to_rbs).to eq('bool') - end + it 'detects namespace and scope for simple types' do + types = Solargraph::ComplexType.parse 'Class' + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('Class') + expect(types.first.scope).to eq(:instance) + expect(types.to_rbs).to eq('Class') + end - it "does not typify non-rooted Booleans" do - api_map = double(Solargraph::ApiMap, qualify: nil) - type = Solargraph::ComplexType.parse('Boolean') - expect(type.rooted_tag).to eq('Boolean') - expect(type.to_rbs).to eq('bool') - end + it 'detects namespace and scope for classes with subtypes' do + types = Solargraph::ComplexType.parse 'Class' + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('String') + expect(types.first.scope).to eq(:class) + # RBS doesn't support individual class types like this + expect(types.to_rbs).to eq('Class') + end - it "returns undefined for unqualified types" do - api_map = double(Solargraph::ApiMap, qualify: nil) - type = Solargraph::ComplexType.parse('UndefinedClass') - qualified = type.qualify(api_map) - expect(qualified).to be_undefined - expect(qualified.to_rbs).to eq('untyped') - end + it 'detects namespace and scope for modules with subtypes' do + types = Solargraph::ComplexType.parse 'Module' + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('Foo') + expect(types.first.scope).to eq(:class) + expect(types.to_rbs).to eq('Module') + multiple_types = Solargraph::ComplexType.parse 'Module, Class, String, nil' + expect(multiple_types.length).to eq(4) + expect(multiple_types.namespaces).to eq(%w[Foo Bar String NilClass]) + # RBS doesn't support individual module types like this + expect(multiple_types.to_rbs).to eq('(Module | Class | String | nil)') + end + end - it 'reports selfy types' do - type = Solargraph::ComplexType.parse('self') - expect(type).to be_selfy - expect(type.to_rbs).to eq('self') - end + context 'simplifies type representation on output' do + it 'throws away other types when in union with an undefined' do + type = Solargraph::ComplexType.parse('Symbol, String, Array(Integer, Integer), undefined') + expect(type.to_s).to eq('undefined') + end - it 'reports selfy parameter types' do - type = Solargraph::ComplexType.parse('Class') - expect(type).to be_selfy - expect(type.to_rbs).to eq('Class') - end + it 'deduplicates types that are implicit unions' do + type = Solargraph::ComplexType.parse('Array') + expect(type.to_s).to eq('Array') + end - it 'resolves self keywords in types' do - selfy = Solargraph::ComplexType.parse('self') - type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) - expect(type.tag).to eq('Foo') - end + it "does not deduplicate types that aren't implicit unions" do + type = Solargraph::ComplexType.parse('Foo') + expect(type.to_s).to eq('Foo') + end - it 'resolves self keywords in parameter types' do - selfy = Solargraph::ComplexType.parse('Array') - type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) - expect(type.tag).to eq('Array') - end + it 'squashes literal types when simplifying literals of same type' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('1, 2, 3') + type = type.qualify(api_map) + expect(type.to_s).to eq('1, 2, 3') + expect(type.tags).to eq('1, 2, 3') + expect(type.simple_tags).to eq('Integer') + expect(type.to_rbs).to eq('(1 | 2 | 3)') + end + end - it 'resolves self keywords in hash parameter types' do - selfy = Solargraph::ComplexType.parse('Hash{String => self}') - type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) - expect(type.tag).to eq('Hash{String => Foo}') - expect(type.to_rbs).to eq('Hash[String, Foo]') - end + it 'identifies nil types regardless of capitalization' do + %w[nil Nil NIL].each do |t| + types = Solargraph::ComplexType.parse(t) + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('NilClass') + expect(types.first.scope).to eq(:instance) + expect(types.first.nil_type?).to be(true) + expect(types.to_rbs).to eq('nil') + end + end - it 'resolves self keywords in ordered array types' do - selfy = Solargraph::ComplexType.parse('Array<(String, Symbol, self)>') - type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) - expect(type.tag).to eq('Array<(String, Symbol, Foo)>') - expect(type.to_rbs).to eq('Array[[String, Symbol, Foo]]') - end + context 'defines rooted and unrooted concept' do + it 'identify rooted types' do + types = Solargraph::ComplexType.parse '::Array' + expect(types.map(&:rooted?)).to eq([true]) + expect(types.to_rbs).to eq('::Array') + end - it 'qualifies special types' do - api_map = Solargraph::ApiMap.new - type = Solargraph::ComplexType.parse('nil') - qual = type.qualify(api_map) - expect(qual.tag).to eq('nil') - expect(qual.to_rbs).to eq('nil') - end + it 'identify unrooted types' do + types = Solargraph::ComplexType.parse 'Array' + expect(types.map(&:rooted?)).to eq([false]) + end - it 'parses a complex subtype' do - type = Solargraph::ComplexType.parse('Array').self_to_type(Solargraph::ComplexType.parse('Foo')) - expect(type.tag).to eq('Array>') - expect(type.to_rbs).to eq('Array[Foo[String]]') - end + ['generic', 'nil', 'true', 'false', ':123', '123'].each do |tag| + it "treats #{tag} as rooted" do + types = Solargraph::ComplexType.parse(tag) + expect(types.all?(&:rooted?)).to be(true) + end + end + end - it 'recognizes param types' do - type = Solargraph::ComplexType.parse('generic') - expect(type).to be_generic - expect(type.to_rbs).to eq('Variable') - end + context 'allows users to define their own generic types' do + it 'recognizes param types' do + type = Solargraph::ComplexType.parse('generic') + expect(type).to be_generic + expect(type.to_rbs).to eq('Variable') + end - it 'recognizes generic parameters' do - type = Solargraph::ComplexType.parse('Array>') - expect(type).to be_generic - expect(type.to_rbs).to eq('Array[Variable]') - end + it 'recognizes generic parameters' do + type = Solargraph::ComplexType.parse('Array>') + expect(type).to be_generic + expect(type.to_rbs).to eq('Array[Variable]') + end - it 'recognizes generic parameters of hash parameter types' do - type = Solargraph::ComplexType.parse('Hash{generic => generic}') - expect(type.tag).to eq('Hash{generic => generic}') - expect(type.to_rbs).to eq('Hash[Variable, Other]') - end + it 'recognizes generic parameters of hash parameter types' do + type = Solargraph::ComplexType.parse('Hash{generic => generic}') + expect(type.tag).to eq('Hash{generic => generic}') + expect(type.to_rbs).to eq('Hash[Variable, Other]') + end - it 'reduces objects' do - api_map = Solargraph::ApiMap.new - selfy = Solargraph::ComplexType.parse('Array') - type = selfy.self_to_type(Solargraph::ComplexType.parse('String')) - expect(type.tag).to eq('Array') - expect(type.to_rbs).to eq('Array[String]') - result = type.qualify(api_map) - expect(result.tag).to eq('Array') - expect(result.to_rbs).to eq('::Array[::String]') - end + it 'resolves generic namespace parameters' do + return_type = Solargraph::ComplexType.parse('Array>') + generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: '@generic GenericTypeParam') + called_method = Solargraph::Pin::Method.new( + location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), + closure: generic_class, + name: 'bar', + comments: '@return [Foo]' + ) + type = return_type.resolve_generics(generic_class, called_method.return_type) + expect(type.tag).to eq('Array') + end - UNIQUE_METHOD_GENERIC_TESTS = [ - # tag, context_type_tag, unfrozen_input_map, expected_tag, expected_output_map - ['String', 'String', {}, 'String', {}], - ['generic', 'String', {}, 'String', {'A' => 'String'}], - ['generic', 'Array', {}, 'Array', {'A' => 'Array'}], - ['generic', 'Array', {'A' => 'String'}, 'String', {'A' => 'String'}], - ['generic', 'Array>', {'B' => 'Integer'}, 'Array', {'B' => 'Integer', 'A' => 'Array'}], - ['Array>', 'Array', {}, 'Array', {'A' => 'String'}], - ] - - UNIQUE_METHOD_GENERIC_TESTS.each do |tag, context_type_tag, unfrozen_input_map, expected_tag, expected_output_map| - context "resolves #{tag} with context #{context_type_tag} and existing resolved generics #{unfrozen_input_map}" do - let(:complex_type) { Solargraph::ComplexType.parse(tag) } - let(:unique_type) { unique_type = complex_type.first } - - it '#{tag} is a unique type' do - expect(complex_type.length).to eq(1) + it 'resolves generic parameters on a tuple using ()' do + return_type = Solargraph::ComplexType.parse('Array(generic, generic)') + generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', + comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") + called_method = Solargraph::Pin::Method.new( + location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), + closure: generic_class, + name: 'bar', + comments: '@return [Foo]' + ) + type = return_type.resolve_generics(generic_class, called_method.return_type) + expect(type.tag).to eq('Array(String, Integer)') end - let(:generic_value) { unfrozen_input_map.transform_values! { |tag| Solargraph::ComplexType.parse(tag) } } - let(:context_type) { Solargraph::ComplexType.parse(context_type_tag) } + it 'resolves generic parameters on a tuple using <()>' do + return_type = Solargraph::ComplexType.parse('Array<(generic, generic)>') + generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', + comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") + called_method = Solargraph::Pin::Method.new( + location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), + closure: generic_class, + name: 'bar', + comments: '@return [Foo]' + ) + type = return_type.resolve_generics(generic_class, called_method.return_type) + expect(type.tag).to eq('Array<(String, Integer)>') + end + + UNIQUE_METHOD_GENERIC_TESTS = [ + # tag, context_type_tag, unfrozen_input_map, expected_tag, expected_output_map + ['String', 'String', {}, 'String', {}], + ['generic', 'String', {}, 'String', { 'A' => 'String' }], + ['generic', 'Array', {}, 'Array', { 'A' => 'Array' }], + ['generic', 'Array', { 'A' => 'String' }, 'String', { 'A' => 'String' }], + ['generic', 'Array>', { 'B' => 'Integer' }, 'Array', + { 'B' => 'Integer', 'A' => 'Array' }], + ['Array>', 'Array', {}, 'Array', { 'A' => 'String' }] + ] + + UNIQUE_METHOD_GENERIC_TESTS.each do |tag, context_type_tag, unfrozen_input_map, expected_tag, expected_output_map| + context "resolves #{tag} with context #{context_type_tag} and existing resolved generics #{unfrozen_input_map}" do + let(:complex_type) { Solargraph::ComplexType.parse(tag) } + let(:unique_type) { complex_type.first } + + it '#{tag} is a unique type' do + expect(complex_type.length).to eq(1) + end + + let(:generic_value) { unfrozen_input_map.transform_values! { |tag| Solargraph::ComplexType.parse(tag) } } + let(:context_type) { Solargraph::ComplexType.parse(context_type_tag) } - it "resolves to #{expected_tag} with updated map #{expected_output_map}" do - resolved_generic_values = unfrozen_input_map.transform_values { |tag| Solargraph::ComplexType.parse(tag) } - resolved_type = unique_type.resolve_generics_from_context(expected_output_map.keys, context_type, resolved_generic_values: resolved_generic_values) - expect(resolved_type.tag).to eq(expected_tag) - expect(resolved_generic_values.transform_values(&:tag)).to eq(expected_output_map) + it "resolves to #{expected_tag} with updated map #{expected_output_map}" do + resolved_generic_values = unfrozen_input_map.transform_values { |tag| Solargraph::ComplexType.parse(tag) } + resolved_type = unique_type.resolve_generics_from_context(expected_output_map.keys, context_type, + resolved_generic_values: resolved_generic_values) + expect(resolved_type.tag).to eq(expected_tag) + expect(resolved_generic_values.transform_values(&:tag)).to eq(expected_output_map) + end + end end end end - it 'resolves generic namespace parameters' do - return_type = Solargraph::ComplexType.parse('Array>') - generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: '@generic GenericTypeParam') - called_method = Solargraph::Pin::Method.new( - location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), - closure: generic_class, - name: 'bar', - comments: '@return [Foo]' - ) - type = return_type.resolve_generics(generic_class, called_method.return_type) - expect(type.tag).to eq('Array') - end + context 'identifies type of parameter syntax used' do + it 'raises NoMethodError for missing methods' do + type = Solargraph::ComplexType.parse('String') + expect { type.undefined_method }.to raise_error(NoMethodError) + end - it 'resolves generic parameters on a tuple using ()' do - return_type = Solargraph::ComplexType.parse('Array(generic, generic)') - generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") - called_method = Solargraph::Pin::Method.new( - location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), - closure: generic_class, - name: 'bar', - comments: '@return [Foo]' - ) - type = return_type.resolve_generics(generic_class, called_method.return_type) - expect(type.tag).to eq('Array(String, Integer)') - end + it 'identifies list parameter types' do + types = Solargraph::ComplexType.parse('Array') + expect(types.first.list_parameters?).to be(true) + expect(types.to_rbs).to eq('Array[String, Symbol]') + end - it 'resolves generic parameters on a tuple using <()>' do - return_type = Solargraph::ComplexType.parse('Array<(generic, generic)>') - generic_class = Solargraph::Pin::Namespace.new(name: 'Foo', comments: "@generic GenericTypeParam1\n@generic GenericTypeParam2") - called_method = Solargraph::Pin::Method.new( - location: Solargraph::Location.new('file:///foo.rb', Solargraph::Range.from_to(0, 0, 0, 0)), - closure: generic_class, - name: 'bar', - comments: '@return [Foo]' - ) - type = return_type.resolve_generics(generic_class, called_method.return_type) - expect(type.tag).to eq('Array<(String, Integer)>') - end + it 'identifies fixed parameters' do + types = Solargraph::ComplexType.parse('Array(String, Symbol)') + expect(types.first.fixed_parameters?).to be(true) + expect(types.first.subtypes.map(&:namespace)).to eq(%w[String Symbol]) + # RBS doesn't use a type name for tuples, just the [] shorthand + expect(types.to_rbs).to eq('[String, Symbol]') + end - # See literal details at - # https://github.com/ruby/rbs/blob/master/docs/syntax.md and - # https://yardoc.org/types.html - xit 'understands literal strings with double quotes' do - type = Solargraph::ComplexType.parse('"foo"') - expect(type.tag).to eq('"foo"') - expect(type.to_rbs).to eq('"foo"') - expect(type.to_s).to eq('String') + it 'identifies hash parameters' do + types = Solargraph::ComplexType.parse('Hash{String => Integer}') + expect(types.length).to eq(1) + expect(types.first.hash_parameters?).to be(true) + end end - xit 'understands literal strings with single quotes' do - type = Solargraph::ComplexType.parse("'foo'") - expect(type.tag).to eq("'foo'") - expect(type.to_rbs).to eq("'foo'") - expect(type.to_s).to eq('String') + context "'qualifies' types by resolving relative references to types to absolute references (fully qualified types)" do + it 'returns undefined for unqualified types' do + api_map = double(Solargraph::ApiMap, qualify: nil) + type = Solargraph::ComplexType.parse('UndefinedClass') + qualified = type.qualify(api_map) + expect(qualified).to be_undefined + expect(qualified.to_rbs).to eq('untyped') + end end - it 'understands literal symbols' do - type = Solargraph::ComplexType.parse(':foo') - expect(type.tag).to eq(':foo') - expect(type.to_rbs).to eq(':foo') - expect(type.to_s).to eq(':foo') + context 'allows list-of-types to be destructively cast down to a single type' do + it 'returns the first type when multiple were parsed with #tag' do + type = Solargraph::ComplexType.parse('String, Array') + expect(type.tag).to eq('String') + expect(type.to_rbs).to eq('(String | Array[String])') + end end - it 'understands literal integers' do - type = Solargraph::ComplexType.parse('123') - expect(type.tag).to eq('123') - expect(type.to_rbs).to eq('123') - expect(type.to_s).to eq('123') - end + context "supports arbitrary combinations of the above syntax and features" do + it 'returns string representations of the entire type array' do + type = Solargraph::ComplexType.parse('String', 'Array') + expect(type.to_s).to eq('String, Array') + # we want this surrounded by () so that it can be composed with + # other types without worrying about operator precedence + expect(type.to_rbs).to eq('(String | Array[String])') + end - it 'understands literal true' do - type = Solargraph::ComplexType.parse('true') - expect(type.tag).to eq('true') - expect(type.to_rbs).to eq('true') - expect(type.to_s).to eq('true') - end + it 'parses recursive subtypes' do + types = Solargraph::ComplexType.parse('Array Integer}>') + expect(types.length).to eq(1) + expect(types.first.namespace).to eq('Array') + expect(types.first.substring).to eq(' Integer}>') + expect(types.first.subtypes.length).to eq(1) + expect(types.first.subtypes.first.namespace).to eq('Hash') + expect(types.first.subtypes.first.substring).to eq('{String => Integer}') + expect(types.first.subtypes.first.key_types.map(&:namespace)).to eq(['String']) + expect(types.first.subtypes.first.value_types.map(&:namespace)).to eq(['Integer']) + expect(types.to_rbs).to eq('Array[Hash[String, Integer]]') + end - it 'understands literal false' do - type = Solargraph::ComplexType.parse('false') - expect(type.tag).to eq('false') - expect(type.to_rbs).to eq('false') - expect(type.to_s).to eq('false') - end + it 'allows various parameterized types as parameterized type' do + types = Solargraph::ComplexType.parse('Array, Hash{String => Symbol}, Array(String, Integer)') + expect(types.all?(&:parameters?)).to be(true) + expect(types.to_rbs).to eq('(Array[String] | Hash[String, Symbol] | [String, Integer])') + end - it 'parses tuples of tuples' do - type = Solargraph::ComplexType.parse('Array(Array(String), String)') - expect(type.tag).to eq('Array(Array(String), String)') - expect(type.to_rbs).to eq('[[String], String]') - expect(type.to_s).to eq('Array(Array(String), String)') - end + let(:foo_bar_api_map) do + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + module Foo + class Bar + # @return [Bar] + def make_bar + end + end + end + )) + api_map.map source + api_map + end - it 'parses tuples of tuples with same type twice in a row' do - type = Solargraph::ComplexType.parse('Array(Symbol, String, Array(Integer, Integer))') - expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') - expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') - expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') - end + it 'qualifies types with list parameters' do + original = Solargraph::ComplexType.parse('Class').first + expect(original).not_to be_rooted + qualified = original.qualify(foo_bar_api_map, 'Foo') + expect(qualified.tag).to eq('Class') + expect(qualified.rooted_tag).to eq('::Class<::Foo::Bar>') + expect(qualified).to be_rooted + expect(qualified.to_rbs).to eq('::Class') + end - it 'qualifies tuples of tuples with same type twice in a row' do - api_map = Solargraph::ApiMap.new - type = Solargraph::ComplexType.parse('Array(Symbol, String, Array(Integer, Integer))') - type = type.qualify(api_map) - expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') - expect(type.to_rbs).to eq('[::Symbol, ::String, [::Integer, ::Integer]]') - end + it 'qualifies types with fixed parameters' do + original = Solargraph::ComplexType.parse('Array(String, Bar)').first + expect(original.to_rbs).to eq('[String, Bar]') + qualified = original.qualify(foo_bar_api_map, 'Foo') + expect(qualified).to be_rooted + expect(qualified.tag).to eq('Array(String, Foo::Bar)') + expect(qualified.to_rbs).to eq('[::String, ::Foo::Bar]') + end - it 'throws away other types when in union with an undefined' do - type = Solargraph::ComplexType.parse('Symbol, String, Array(Integer, Integer), undefined') - expect(type.to_s).to eq('undefined') - end + it 'qualifies types with hash parameters' do + original = Solargraph::ComplexType.parse('Hash{String => Bar}').first + qualified = original.qualify(foo_bar_api_map, 'Foo') + expect(qualified.tag).to eq('Hash{String => Foo::Bar}') + expect(qualified.to_rbs).to eq('::Hash[::String, ::Foo::Bar]') + end - it 'deduplicates types that are implicit unions' do - type = Solargraph::ComplexType.parse('Array') - expect(type.to_s).to eq('Array') - end + it 'parses tuples of tuples with same type twice in a row' do + type = Solargraph::ComplexType.parse('Array(Symbol, String, Array(Integer, Integer))') + expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') + expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') + expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') + end - it "does not deduplicate types that aren't implicit unions" do - type = Solargraph::ComplexType.parse('Foo') - expect(type.to_s).to eq('Foo') - end + it 'qualifies tuples of tuples with same type twice in a row' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('Array(Symbol, String, Array(Integer, Integer))') + type = type.qualify(api_map) + expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') + expect(type.to_rbs).to eq('[::Symbol, ::String, [::Integer, ::Integer]]') + end - it 'squashes literal types when simplifying literals of same type' do - api_map = Solargraph::ApiMap.new - type = Solargraph::ComplexType.parse('1, 2, 3') - type = type.qualify(api_map) - expect(type.to_s).to eq('1, 2, 3') - expect(type.tags).to eq('1, 2, 3') - expect(type.simple_tags).to eq('Integer') - expect(type.to_rbs).to eq('(1 | 2 | 3)') - end + it 'qualifies special types' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('nil') + qual = type.qualify(api_map) + expect(qual.tag).to eq('nil') + expect(qual.to_rbs).to eq('nil') + end - xit 'stops parsing when the first character indicates a string literal' do - api_map = Solargraph::ApiMap.new - type = Solargraph::ComplexType.parse('"Array(Symbol, String, Array(Integer, Integer)"') - type = type.qualify(api_map) - expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') - expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') - expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') - end + it 'resolves self keywords in parameter types' do + selfy = Solargraph::ComplexType.parse('Array') + type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) + expect(type.tag).to eq('Array') + end + + it 'resolves self keywords in hash parameter types' do + selfy = Solargraph::ComplexType.parse('Hash{String => self}') + type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) + expect(type.tag).to eq('Hash{String => Foo}') + expect(type.to_rbs).to eq('Hash[String, Foo]') + end + + it 'resolves self keywords in ordered array types' do + selfy = Solargraph::ComplexType.parse('Array<(String, Symbol, self)>') + type = selfy.self_to_type(Solargraph::ComplexType.parse('Foo')) + expect(type.tag).to eq('Array<(String, Symbol, Foo)>') + expect(type.to_rbs).to eq('Array[[String, Symbol, Foo]]') + end + + it 'understands self types as subtypes' do + api_map = Solargraph::ApiMap.new + selfy = Solargraph::ComplexType.parse('Array') + type = selfy.self_to_type(Solargraph::ComplexType.parse('String')) + expect(type.tag).to eq('Array') + expect(type.to_rbs).to eq('Array[String]') + result = type.qualify(api_map) + expect(result.tag).to eq('Array') + expect(result.to_rbs).to eq('::Array[::String]') + end - ['generic', "nil", "true", "false", ":123", "123"].each do |tag| - it "treats #{tag} as rooted" do - types = Solargraph::ComplexType.parse(tag) - expect(types.all?(&:rooted?)).to be(true) + xit 'stops parsing when the first character indicates a string literal' do + api_map = Solargraph::ApiMap.new + type = Solargraph::ComplexType.parse('"Array(Symbol, String, Array(Integer, Integer)"') + type = type.qualify(api_map) + expect(type.tag).to eq('Array(Symbol, String, Array(Integer, Integer))') + expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') + expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') end end end From 18f3282771314a640863c5d9d2ca327037e9e5ec Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 15 Jun 2025 12:20:09 -0400 Subject: [PATCH 075/116] Fix bug, add @todo for typechecker --- lib/solargraph/shell.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 5b0588b7a..67be4e251 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -257,7 +257,9 @@ def pin_description pin # @return [void] def do_cache gemspec api_map = ApiMap.load('.') - api_map.cache_gem(gemspec, options.rebuild, out: $stdout) + # @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) end end end From a6e42f21f970b34dccad2c5067bd25784bb4c78f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 15 Jun 2025 12:29:50 -0400 Subject: [PATCH 076/116] Add some type tags --- lib/solargraph/pin/method.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 17747a863..1e83218eb 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -31,6 +31,7 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @anon_splat = anon_splat end + # @return [Array] def combine_all_signature_pins(*signature_pins) by_arity = {} signature_pins.each do |signature_pin| @@ -46,6 +47,8 @@ def combine_all_signature_pins(*signature_pins) by_arity.values.flatten end + # @param other [Pin::Method] + # @return [Symbol] def combine_visibility(other) if dodgy_visibility_source? && !other.dodgy_visibility_source? other.visibility @@ -56,6 +59,8 @@ def combine_visibility(other) end end + # @param other [Pin::Method] + # @return [Array] def combine_signatures(other) all_undefined = signatures.all? { |sig| sig.return_type.undefined? } other_all_undefined = other.signatures.all? { |sig| sig.return_type.undefined? } @@ -89,6 +94,7 @@ def combine_with(other, attrs = {}) super(other, new_attrs) end + # @param other [Pin::Method] def == other super && other.node == node end @@ -104,6 +110,7 @@ def transform_types(&transform) m end + # @return [void] def reset_generated! super unless signatures.empty? @@ -206,6 +213,8 @@ def signatures end end + # @param return_type [ComplexType] + # @return [self] def proxy_with_signatures return_type out = proxy return_type out.signatures = out.signatures.map { |sig| sig.proxy return_type } @@ -263,6 +272,7 @@ def path @path ||= "#{namespace}#{(scope == :instance ? '#' : '.')}#{name}" end + # @return [String] def method_name name end @@ -423,6 +433,7 @@ def resolve_ref_tag api_map end # @param api_map [ApiMap] + # @return [Array] def rest_of_stack api_map api_map.get_method_stack(method_namespace, method_name, scope: scope).reject { |pin| pin.path == path } end @@ -512,6 +523,7 @@ def see_reference api_map resolve_reference match[1], api_map end + # @return [String] def method_namespace namespace end From 314d667c7f2e40ae2650ec102db78dafae7b8646 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 15 Jun 2025 12:31:26 -0400 Subject: [PATCH 077/116] Fix issue found in typechecking --- lib/solargraph/pin/method.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 1e83218eb..9b2f7e934 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -452,7 +452,7 @@ def dodgy_visibility_source? # as of 2025-03-12, the RBS generator used for # e.g. activesupport did not understand 'private' markings # inside 'class << self' blocks, but YARD did OK at it - source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_value.undefined? || + source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_type.undefined? || # YARD's RBS generator seems to miss a lot of should-be protected instance methods source == :rbs && scope == :instance && namespace.start_with?('YARD::') || # private on attr_readers seems to be broken in Prism's auto-generator script From 21c18c8d728532696074821a4eb54862592d6057 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 15 Jun 2025 12:48:03 -0400 Subject: [PATCH 078/116] Log during other gem caching operations too --- lib/solargraph/doc_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index a6d850c74..ba63479e7 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -55,7 +55,6 @@ def cache_all!(out) logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" } load_serialized_gem_pins uncached_gemspecs.each do |gemspec| - out.puts "Caching pins for gem #{gemspec.name}:#{gemspec.version}" cache(gemspec, out: out) end load_serialized_gem_pins @@ -81,6 +80,7 @@ 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 end From f2afc3fc2cdf14458dfcfd34bfe7e20c32ea444b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 17 Jun 2025 09:54:36 -0400 Subject: [PATCH 079/116] Handle a nil case in Pin::Base#assert_same --- lib/solargraph/pin/base.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 9be9e7ab4..a41c60c57 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -225,6 +225,7 @@ def assert_same_count(other, attr) # # @return [Object, nil] def assert_same(other, attr) + return false if other.nil? val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 From 28c16bc362205bb176878d3b96a20012f5f4135c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 17 Jun 2025 11:15:23 -0400 Subject: [PATCH 080/116] Add annotations for strong typechecking on Pin::Base --- lib/solargraph/pin/base.rb | 90 +++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index a41c60c57..f79e7ce89 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -38,6 +38,8 @@ def presence_certain? # @param name [String] # @param comments [String] # @param source [Symbol, nil] + # @param docstring [YARD::Docstring, nil] + # @param directives [::Array, nil] def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', docstring: nil, directives: nil @location = location @type_location = type_location @@ -77,20 +79,31 @@ def combine_with(other, attrs={}) out end + # @param other [self] + # @param attr [::Symbol] + # @sg-ignore + # @return [undefined] def choose_longer(other, attr) + # @type [undefined] val1 = send(attr) + # @type [undefined] val2 = other.send(attr) return val1 if val1 == val2 return val2 if val1.nil? + # @sg-ignore val1.length > val2.length ? val1 : val2 end + # @param other [self] + # @return [::Array, nil] def combine_directives(other) return self.directives if other.directives.empty? return other.directives if directives.empty? [directives + other.directives].uniq end + # @param other [self] + # @return [String] def combine_name(other) if needs_consistent_name? || other.needs_consistent_name? assert_same(other, :name) @@ -99,6 +112,7 @@ def combine_name(other) end end + # @return [void] def reset_generated! # @return_type doesn't go here as subclasses tend to assign it # themselves in constructors, and they will deal with setting @@ -123,6 +137,8 @@ def needs_consistent_name? [name, location, type_location, closure, source] end + # @param other [self] + # @return [ComplexType] def combine_return_type(other) if return_type.undefined? other.return_type @@ -147,6 +163,7 @@ def dodgy_return_type_source? location&.filename&.include?('core_ext/object/') end + # @param p1 [self] def <=>(p1) return nil unless p1.is_a?(self.class) return 0 if self == p1 @@ -169,6 +186,10 @@ def choose(other, attr) raise end + # @param other [self] + # @param attr [Symbol] + # @sg-ignore + # @return [undefined] def choose_node(other, attr) if other.object_id < attr.object_id other.send(attr) @@ -177,6 +198,10 @@ def choose_node(other, attr) end end + # @param other [self] + # @param attr [::Symbol] + # @sg-ignore + # @return [undefined] def prefer_rbs_location(other, attr) if rbs_location? && !other.rbs_location? self.send(attr) @@ -191,16 +216,31 @@ def rbs_location? type_location&.rbs? end + # @param other [self] + # @return [void] def assert_same_macros(other) assert_same_count(other, :macros) assert_same_array_content(other, :macros) { |macro| macro.tag.name } end + # @param other [self] + # @param attr [::Symbol] + # @return [void] + # @todo strong typechecking should complain when there are no block-related tags def assert_same_array_content(other, attr, &block) arr1 = send(attr) + raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable) + # @type arr1 [::Enumerable] arr2 = other.send(attr) + raise "Expected #{attr} on #{other} to be an Enumerable, got #{arr2.class}" unless arr2.is_a?(::Enumerable) + # @type arr2 [::Enumerable] + + # @sg-ignore + # @type [undefined] values1 = arr1.map(&block) + # @type [undefined] values2 = arr2.map(&block) + # @sg-ignore return arr1 if values1 == values2 Solargraph.assert_or_log("combine_with_#{attr}".to_sym, "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self values = #{values1}\nother values =#{attr} = #{values2}") @@ -210,14 +250,18 @@ def assert_same_array_content(other, attr, &block) # @param other [self] # @param attr [::Symbol] # - # @return [Object, nil] + # @return [::Enumerable] def assert_same_count(other, attr) - val1 = send(attr) - val2 = other.send(attr) - return val1 if val1.count == val2.count + # @type [::Enumerable] + arr1 = self.send(attr) + raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable) + # @type [::Enumerable] + arr2 = other.send(attr) + raise "Expected #{attr} on #{other} to be an Enumerable, got #{arr2.class}" unless arr2.is_a?(::Enumerable) + return arr1 if arr1.count == arr2.count Solargraph.assert_or_log("combine_with_#{attr}".to_sym, - "Inconsistent #{attr.inspect} count value between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") - val1 + "Inconsistent #{attr.inspect} count value between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{arr1.inspect}\nother.#{attr} = #{arr2.inspect}") + arr1 end # @param other [self] @@ -234,9 +278,17 @@ def assert_same(other, attr) val1 end + # @param other [self] + # @param attr [::Symbol] + # @sg-ignore + # @return [undefined] def choose_pin_attr_with_same_name(other, attr) + # @type [Pin::Base, nil] val1 = send(attr) + # @type [Pin::Base, nil] val2 = other.send(attr) + raise "Expected pin for #{attr} on\n#{self.inspect},\ngot #{val1.inspect}" unless val1.nil? || val1.is_a?(Pin::Base) + raise "Expected pin for #{attr} on\n#{other.inspect},\ngot #{val2.inspect}" unless val2.nil? || val2.is_a?(Pin::Base) if val1&.name != val2&.name Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") @@ -244,30 +296,6 @@ def choose_pin_attr_with_same_name(other, attr) [val1, val2].compact.min end - # @param other [self] - # @param attr [::Symbol] - # - # @return [Pin::Base, nil] - def assert_combined_pin(other, attr) - val1 = send(attr) - val2 = other.send(attr) - if val1.nil? && val2.nil? - return nil - end - unless val1 && val2 - Solargraph.assert_or_log(:combine_with, - "Inconsistent #{attr.inspect} values from \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") - return val1 || val2 - end - if val1.object_id == self.object_id || val2.object_id == self.object_id - Solargraph.assert_or_log(:combine_with, - "Loop found: #{val1.inspect} or #{val2.inspect} may be same as self - #{inspect}") - return nil - end - - val1.combine_with(val2) - end - # @return [String] def comments @comments ||= '' @@ -362,6 +390,7 @@ def nearly? other # Pin equality is determined using the #nearly? method and also # requiring both pins to have the same location. # + # @param other [self] def == other return false unless nearly? other comments == other.comments && location == other.location @@ -507,6 +536,7 @@ def desc "[name=#{name.inspect} return_type=#{type_desc}, context=#{context.rooted_tags}, closure=#{closure_info}, binder=#{binder_info}]" end + # @return [String] def inspect "#<#{self.class} `#{self.desc}` at #{self.location.inspect}>" end From 891b2249e658e0fcaab554eb134aa5c5861df546 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 17 Jun 2025 11:47:19 -0400 Subject: [PATCH 081/116] Handle a block combination situation --- lib/solargraph/pin/callable.rb | 10 ++++++++-- lib/solargraph/pin/parameter.rb | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 68ee2ce5f..7930a6ccd 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -59,8 +59,14 @@ def generics def choose_parameters(other) raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity - parameters.each_with_index.map do |param, i| - param.combine_with(other.parameters[i]) + parameters.zip(other.parameters).map do |param, other_param| + if param.nil? && other_param.block? + other_param + elsif other_param.nil? && param.block? + param + else + param.combine_with(other_param) + end end end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 56cbf3456..56ce3dec3 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -46,6 +46,7 @@ def needs_consistent_name? keyword? end + # @return [String] def arity_decl name = (self.name || '(anon)') type = (return_type&.to_rbs || 'untyped') From 5c79657a936681686804094675e5cd861760a12b Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 23 Jun 2025 09:34:03 -0400 Subject: [PATCH 082/116] Use prism --- lib/solargraph/parser/parser_gem/class_methods.rb | 12 +++++++----- solargraph.gemspec | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index c8ccdaadb..f58595641 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -2,6 +2,7 @@ require 'parser/current' require 'parser/source/buffer' +require 'prism' # Awaiting ability to use a version containing https://github.com/whitequark/parser/pull/1076 # @@ -30,11 +31,12 @@ def parse_with_comments code, filename = nil # @param line [Integer] # @return [Parser::AST::Node] def parse code, filename = nil, line = 0 - buffer = ::Parser::Source::Buffer.new(filename, line) - buffer.source = code - parser.parse(buffer) - rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e - raise Parser::SyntaxError, e.message + # buffer = ::Parser::Source::Buffer.new(filename, line) + # buffer.source = code + # parser.parse(buffer) + # rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e + # raise Parser::SyntaxError, e.message + Prism::Translation::Parser.parse(code, filename, line) end # @return [::Parser::Base] diff --git a/solargraph.gemspec b/solargraph.gemspec index 2bfb6dbdf..5008b6247 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'observer', '~> 0.1' s.add_runtime_dependency 'ostruct', '~> 0.6' s.add_runtime_dependency 'parser', '~> 3.0' + s.add_runtime_dependency 'prism', '~> 1.4' s.add_runtime_dependency 'rbs', '~> 3.3' s.add_runtime_dependency 'reverse_markdown', '~> 3.0' s.add_runtime_dependency 'rubocop', '~> 1.38' From 3c95bf6f0280ac416c536ef45b8649f3792c3d5e Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 23 Jun 2025 10:46:06 -0400 Subject: [PATCH 083/116] Memoize parser --- .../parser/parser_gem/class_methods.rb | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index f58595641..58ca8056b 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'parser/current' -require 'parser/source/buffer' require 'prism' # Awaiting ability to use a version containing https://github.com/whitequark/parser/pull/1076 @@ -31,22 +29,19 @@ def parse_with_comments code, filename = nil # @param line [Integer] # @return [Parser::AST::Node] def parse code, filename = nil, line = 0 - # buffer = ::Parser::Source::Buffer.new(filename, line) - # buffer.source = code - # parser.parse(buffer) - # rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e - # raise Parser::SyntaxError, e.message - Prism::Translation::Parser.parse(code, filename, line) + buffer = ::Parser::Source::Buffer.new(filename, line) + buffer.source = code + parser.parse(buffer) + rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e + raise Parser::SyntaxError, e.message end # @return [::Parser::Base] def parser - # @todo Consider setting an instance variable. We might not need to - # recreate the parser every time we use it. - parser = ::Parser::CurrentRuby.new(FlawedBuilder.new) - parser.diagnostics.all_errors_are_fatal = true - parser.diagnostics.ignore_warnings = true - parser + @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| + parser.diagnostics.all_errors_are_fatal = true + parser.diagnostics.ignore_warnings = true + end end # @param source [Source] From 999ada7f86e9085fedc5b6da1aa7e96a6054b532 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 23 Jun 2025 20:02:26 -0400 Subject: [PATCH 084/116] Fix merge issues --- lib/solargraph.rb | 16 ++++++++-------- lib/solargraph/pin/base.rb | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 749cf791b..df5dc7f8e 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -59,14 +59,14 @@ def self.asserts_on?(type) return false if type == :combine_with_visibility # Pending https://github.com/castwide/solargraph/pull/947 return false if type == :combine_with_closure_name - @asserts_on ||= if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? - false - elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' - true - else - logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" - false - end + if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? + false + elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' + true + else + logger.warn "Unrecognized SOLARGRAPH_ASSERTS value: #{ENV['SOLARGRAPH_ASSERTS']}" + false + end end def self.assert_or_log(type, msg = nil, &block) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 88fd8543d..77db77f1e 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_source_provided end # @param other [self] @@ -293,7 +294,6 @@ def choose_pin_attr_with_same_name(other, attr) "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end [val1, val2].compact.min - assert_source_provided end def assert_source_provided From 380e8677d485cef9158dbaaafc878489e75ea4ed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 23 Jun 2025 20:16:04 -0400 Subject: [PATCH 085/116] Populate locations in additional, uh, spots --- lib/solargraph/gem_pins.rb | 1 + lib/solargraph/pin/method.rb | 6 ++++-- lib/solargraph/rbs_map/conversions.rb | 22 ++++++++++++++-------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 1713bbace..8edee9a0b 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -42,6 +42,7 @@ def self.combine(yard_pins, rbs_map) generics: rbs.generics, node: yard.node, signatures: yard.signatures, + type_location: rbs.type_location, return_type: best_return_type(rbs.return_type, yard.return_type), source: :gem_pins ) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 2eb488b86..5e223c4e6 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -121,9 +121,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) + block = Signature.new(generics: generics, parameters: yield_parameters, return_type: yield_return_type, source: source, + location: location, type_location: type_location) end - Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, source: source) + Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, source: source, + location: location, type_location: type_location) end # @return [::Array] diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 2f1974df0..5684a5308 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -329,13 +329,16 @@ def method_def_to_pin decl, closure # @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, source: :rbs) + Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, + type_location: type_location) end - Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs) + Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, + type_location: type_location) end end @@ -354,44 +357,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) From 96659a253c2b818833d02ed9b617aca1fc00df68 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 23 Jun 2025 20:31:36 -0400 Subject: [PATCH 086/116] Allow for simplified usage from specs --- lib/solargraph/doc_map.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 26d9b42f7..202eae0dc 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -44,8 +44,8 @@ def initialize(requires, preferences, workspace = nil) @requires = requires.compact @preferences = preferences.compact @workspace = workspace - @rbs_collection_path = workspace.rbs_collection_path - @rbs_collection_config_path = workspace.rbs_collection_config_path + @rbs_collection_path = workspace&.rbs_collection_path + @rbs_collection_config_path = workspace&.rbs_collection_config_path load_serialized_gem_pins end From 53e1ee1e93266616bddb2af56b44f2ca0507207a Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 24 Jun 2025 04:43:46 -0400 Subject: [PATCH 087/116] Nil guards in flow-sensitive typing --- .../parser/flow_sensitive_typing.rb | 6 +- spec/parser/flow_sensitive_typing_spec.rb | 244 ++++++++++++++++++ spec/source_map/clip_spec.rb | 228 ---------------- 3 files changed, 246 insertions(+), 232 deletions(-) create mode 100644 spec/parser/flow_sensitive_typing_spec.rb diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 580222f90..8fb26d498 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -153,8 +153,6 @@ def process_conditional(conditional_node, true_ranges) # @param isa_node [Parser::AST::Node] # @return [Array(String, String)] def parse_isa(isa_node) - # @todo A nil guard might be good enough here, but we might want to - # see if the callers are checking for nils instead. return unless isa_node&.type == :send && isa_node.children[1] == :is_a? # Check if conditional node follows this pattern: # s(:send, @@ -167,12 +165,12 @@ def parse_isa(isa_node) # check if isa_receiver looks like this: # s(:send, nil, :foo) # and set variable_name to :foo - if isa_receiver.type == :send && isa_receiver.children[0].nil? && isa_receiver.children[1].is_a?(Symbol) + if isa_receiver&.type == :send && isa_receiver.children[0].nil? && isa_receiver.children[1].is_a?(Symbol) variable_name = isa_receiver.children[1].to_s end # or like this: # (lvar :repr) - variable_name = isa_receiver.children[0].to_s if isa_receiver.type == :lvar + variable_name = isa_receiver.children[0].to_s if isa_receiver&.type == :lvar return unless variable_name [isa_type_name, variable_name] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb new file mode 100644 index 000000000..538e7c08f --- /dev/null +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +# @todo These tests depend on `Clip`, but we're putting the tests here to +# avoid overloading clip_spec.rb. +describe Solargraph::Parser::FlowSensitiveTyping do + it 'uses is_a? in a simple if() to refine types on a simple class' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @param repr [ReproBase] + def verify_repro(repr) + if repr.is_a?(Repro) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.to_s).to eq('Repro') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses is_a? in a simple if() to refine types on a module-scoped class' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + module Foo + class Repro < ReproBase; end + end + # @param repr [ReproBase] + def verify_repro(repr) + if repr.is_a?(Foo::Repro) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('Foo::Repro') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses is_a? in a simple if() to refine types on a double-module-scoped class' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + module Foo + module Bar + class Repro < ReproBase; end + end + end + # @param repr [ReproBase] + def verify_repro(repr) + if repr.is_a?(Foo::Bar::Repro) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.to_s).to eq('Foo::Bar::Repro') + + clip = api_map.clip_at('test.rb', [12, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses is_a? in a simple unless statement to refine types on a simple class' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @param repr [ReproBase] + def verify_repro(repr) + unless repr.is_a?(Repro) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('Repro') + end + + it 'uses is_a? in an if-then-else() to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro1 < ReproBase; end + # @param repr [ReproBase] + def verify_repro(repr) + if repr.is_a?(Repro1) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.to_s).to eq('Repro1') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses is_a? in a if-then-elsif-else() to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro1 < ReproBase; end + class Repro2 < ReproBase; end + # @param repr [ReproBase] + def verify_repro(repr) + if repr.is_a?(Repro1) + repr + elsif repr.is_a?(Repro2) + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.to_s).to eq('Repro1') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.to_s).to eq('Repro2') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses is_a? in a "break unless" statement in an .each block to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [Array] + foo = bar + foo.each do |value| + break unless value.is_a? Repro + value + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('Repro') + end + + it 'uses is_a? in a "break unless" statement in an until to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase] + value = bar + until is_done() + break unless value.is_a? Repro + value + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('Repro') + end + + it 'uses is_a? in a "break unless" statement in a while to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase] + value = bar + while !is_done() + break unless value.is_a? Repro + value + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('Repro') + end + + it 'uses unless is_a? in a ".each" block to refine types' do + source = Solargraph::Source.load_string(%( + # @type [Array] + arr = [1, 2, 4, 4.5] + arr + arr.each do |value| + value + break unless value.is_a? Float + + value + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [3, 6]) + expect(clip.infer.to_s).to eq('Array') + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.to_s).to eq('Numeric') + + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('Float') + end + + it 'understands compatible reassignments' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [Foo] + def baz; end + end + bar = Foo.new + bar + bar = Foo.new + bar + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 6]) + expect(clip.infer.to_s).to eq('Foo') + + clip = api_map.clip_at('test.rb', [8, 6]) + expect(clip.infer.to_s).to eq('Foo') + end + + it 'skips is_a? without a receiver' do + source = Solargraph::Source.load_string(%( + if is_a? Object + x + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [2, 6]) + expect { clip.infer.to_s }.not_to raise_error + end +end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 09b8c76bd..2d74c54cc 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -2589,234 +2589,6 @@ def foo expect(clip.infer.to_s).to eq('Bar') end - it 'uses is_a? in a simple if() to refine types on a simple class' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro < ReproBase; end - # @param repr [ReproBase] - def verify_repro(repr) - if repr.is_a?(Repro) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) - expect(clip.infer.to_s).to eq('Repro') - - clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - end - - it 'uses is_a? in a simple if() to refine types on a module-scoped class' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - module Foo - class Repro < ReproBase; end - end - # @param repr [ReproBase] - def verify_repro(repr) - if repr.is_a?(Foo::Repro) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.to_s).to eq('Foo::Repro') - - clip = api_map.clip_at('test.rb', [10, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - end - - it 'uses is_a? in a simple if() to refine types on a double-module-scoped class' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - module Foo - module Bar - class Repro < ReproBase; end - end - end - # @param repr [ReproBase] - def verify_repro(repr) - if repr.is_a?(Foo::Bar::Repro) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [10, 10]) - expect(clip.infer.to_s).to eq('Foo::Bar::Repro') - - clip = api_map.clip_at('test.rb', [12, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - end - - it 'uses is_a? in a simple unless statement to refine types on a simple class' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro < ReproBase; end - # @param repr [ReproBase] - def verify_repro(repr) - unless repr.is_a?(Repro) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - - clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.to_s).to eq('Repro') - end - - it 'uses is_a? in an if-then-else() to refine types' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro1 < ReproBase; end - # @param repr [ReproBase] - def verify_repro(repr) - if repr.is_a?(Repro1) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) - expect(clip.infer.to_s).to eq('Repro1') - - clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - end - - it 'uses is_a? in a if-then-elsif-else() to refine types' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro1 < ReproBase; end - class Repro2 < ReproBase; end - # @param repr [ReproBase] - def verify_repro(repr) - if repr.is_a?(Repro1) - repr - elsif repr.is_a?(Repro2) - repr - else - repr - end - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 10]) - expect(clip.infer.to_s).to eq('Repro1') - - clip = api_map.clip_at('test.rb', [9, 10]) - expect(clip.infer.to_s).to eq('Repro2') - - clip = api_map.clip_at('test.rb', [11, 10]) - expect(clip.infer.to_s).to eq('ReproBase') - end - - it 'uses is_a? in a "break unless" statement in an .each block to refine types' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro < ReproBase; end - # @type [Array] - foo = bar - foo.each do |value| - break unless value.is_a? Repro - value - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) - expect(clip.infer.to_s).to eq('Repro') - end - - it 'uses is_a? in a "break unless" statement in an until to refine types' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro < ReproBase; end - # @type [ReproBase] - value = bar - until is_done() - break unless value.is_a? Repro - value - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) - expect(clip.infer.to_s).to eq('Repro') - end - - it 'uses is_a? in a "break unless" statement in a while to refine types' do - source = Solargraph::Source.load_string(%( - class ReproBase; end - class Repro < ReproBase; end - # @type [ReproBase] - value = bar - while !is_done() - break unless value.is_a? Repro - value - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) - expect(clip.infer.to_s).to eq('Repro') - end - - it 'uses unless is_a? in a ".each" block to refine types' do - source = Solargraph::Source.load_string(%( - # @type [Array] - arr = [1, 2, 4, 4.5] - arr - arr.each do |value| - value - break unless value.is_a? Float - - value - end - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [3, 6]) - expect(clip.infer.to_s).to eq('Array') - - clip = api_map.clip_at('test.rb', [5, 8]) - expect(clip.infer.to_s).to eq('Numeric') - - clip = api_map.clip_at('test.rb', [7, 8]) - expect(clip.infer.to_s).to eq('Float') - end - - it 'understands compatible reassignments' do - source = Solargraph::Source.load_string(%( - class Foo - # @return [Foo] - def baz; end - end - bar = Foo.new - bar - bar = Foo.new - bar - ), 'test.rb') - api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 6]) - expect(clip.infer.to_s).to eq('Foo') - - clip = api_map.clip_at('test.rb', [8, 6]) - expect(clip.infer.to_s).to eq('Foo') - end - it 'understands compatible reassignments from same object' do source = Solargraph::Source.load_string(%( class Foo From e4ef9f4046cee8db088613af3cf72f36792e3469 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Sun, 6 Apr 2025 22:38:59 -0400 Subject: [PATCH 088/116] Reimplement global conventions --- lib/solargraph/convention.rb | 6 +++--- lib/solargraph/convention/base.rb | 6 +++--- lib/solargraph/doc_map.rb | 14 +++++++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/solargraph/convention.rb b/lib/solargraph/convention.rb index 2fdaacd1e..b1a1c67e0 100644 --- a/lib/solargraph/convention.rb +++ b/lib/solargraph/convention.rb @@ -31,12 +31,12 @@ def self.for_local(source_map) result end - # @param yard_map [YardMap] + # @param yard_map [DocMap] # @return [Environ] - def self.for_global(yard_map) + def self.for_global(doc_map) result = Environ.new @@conventions.each do |conv| - result.merge conv.global(yard_map) + result.merge conv.global(doc_map) end result end diff --git a/lib/solargraph/convention/base.rb b/lib/solargraph/convention/base.rb index 9c9f28107..ef46e12b4 100644 --- a/lib/solargraph/convention/base.rb +++ b/lib/solargraph/convention/base.rb @@ -20,12 +20,12 @@ def local source_map EMPTY_ENVIRON end - # The Environ for a YARD map. + # The Environ for a DocMap. # Subclasses can override this method. # - # @param yard_map [YardMap] + # @param doc_map [DocMap] # @return [Environ] - def global yard_map + def global doc_map EMPTY_ENVIRON end end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index db0e52150..4fa5001a8 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -8,6 +8,7 @@ class DocMap # @return [Array] attr_reader :requires + alias required requires # @return [Array] attr_reader :preferences @@ -21,14 +22,21 @@ class DocMap # @return [Workspace, nil] attr_reader :workspace + # @return [Environ] + attr_reader :environ + # @param requires [Array] # @param preferences [Array] # @param workspace [Workspace, nil] def initialize(requires, preferences, workspace = nil) @requires = requires.compact @preferences = preferences.compact - @workspace = workspace - generate + @rbs_path = workspace&.rbs_collection_path + @environ = Convention.for_global(self) + @requires.concat @environ.requires + @requires.uniq + generate_gem_pins + pins.concat @environ.pins end # @return [Array] @@ -54,7 +62,7 @@ def dependencies private # @return [void] - def generate + def generate_gem_pins @pins = [] @uncached_gemspecs = [] required_gems_map.each do |path, gemspecs| From 4f1a0d6bd3db0d659ea14ba4e057a76d9cfd53a3 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 7 Apr 2025 00:34:37 -0400 Subject: [PATCH 089/116] Allow for conventions in map specs --- lib/solargraph/api_map.rb | 4 ++++ lib/solargraph/doc_map.rb | 2 -- spec/api_map_spec.rb | 2 +- spec/doc_map_spec.rb | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 4af4b3c88..fefd536d6 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -109,6 +109,10 @@ def catalog bench [self.class, @source_map_hash, implicit, @doc_map, @unresolved_requires] end + def doc_map + @doc_map ||= DocMap.new([], []) + end + # @return [::Array] def uncached_gemspecs @doc_map&.uncached_gemspecs || [] diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 4fa5001a8..376991210 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -33,8 +33,6 @@ def initialize(requires, preferences, workspace = nil) @preferences = preferences.compact @rbs_path = workspace&.rbs_collection_path @environ = Convention.for_global(self) - @requires.concat @environ.requires - @requires.uniq generate_gem_pins pins.concat @environ.pins end diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 8dc6db842..f0a4bce60 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -399,7 +399,7 @@ class Foo; end require 'invalid' ), 'app.rb') @api_map.catalog Solargraph::Bench.new(source_maps: [source1, source2], external_requires: ['invalid']) - expect(@api_map.unresolved_requires).to eq(['invalid']) + expect(@api_map.unresolved_requires).to eq(['invalid'] + @api_map.doc_map.environ.requires) end it 'gets instance variables from superclasses' do diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 2f9d04ff2..f8a8c042d 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -16,7 +16,7 @@ it 'tracks unresolved requires' do doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to eq(['not_a_gem']) + expect(doc_map.unresolved_requires).to eq(['not_a_gem'] + doc_map.environ.requires) end it 'tracks uncached_gemspecs' do @@ -24,7 +24,7 @@ spec.name = 'not_a_gem' spec.version = '1.0.0' end - allow(Gem::Specification).to receive(:find_by_path).with('not_a_gem').and_return(gemspec) + allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) doc_map = Solargraph::DocMap.new(['not_a_gem'], [gemspec]) expect(doc_map.uncached_gemspecs).to eq([gemspec]) end @@ -40,7 +40,7 @@ it 'does not warn for redundant requires' do # Requiring 'set' is unnecessary because it's already included in core. It # might make sense to log redundant requires, but a warning is overkill. - expect(Solargraph.logger).not_to receive(:warn) + expect(Solargraph.logger).not_to receive(:warn).with(/path set/) Solargraph::DocMap.new(['set'], []) end From 883025c5a7b0275eb111d5bd216fa2c0dd31c941 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 24 Jun 2025 06:03:56 -0400 Subject: [PATCH 090/116] Workspace setting --- lib/solargraph/doc_map.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 376991210..1843f5596 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -31,6 +31,7 @@ class DocMap def initialize(requires, preferences, workspace = nil) @requires = requires.compact @preferences = preferences.compact + @workspace = workspace @rbs_path = workspace&.rbs_collection_path @environ = Convention.for_global(self) generate_gem_pins @@ -224,6 +225,7 @@ def gemspecs_required_from_bundler def gemspecs_required_from_external_bundle logger.info 'Fetching gemspecs required from external bundle' + puts workspace.inspect return [] unless workspace&.directory Solargraph.with_clean_env do From e9884b68b4da428702bcdc52f5652b91dec27cb5 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 24 Jun 2025 06:34:17 -0400 Subject: [PATCH 091/116] Environ requires in specs are resolved --- spec/api_map_spec.rb | 2 +- spec/doc_map_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index f0a4bce60..8dc6db842 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -399,7 +399,7 @@ class Foo; end require 'invalid' ), 'app.rb') @api_map.catalog Solargraph::Bench.new(source_maps: [source1, source2], external_requires: ['invalid']) - expect(@api_map.unresolved_requires).to eq(['invalid'] + @api_map.doc_map.environ.requires) + expect(@api_map.unresolved_requires).to eq(['invalid']) end it 'gets instance variables from superclasses' do diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index f8a8c042d..420ee3e90 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -16,7 +16,7 @@ it 'tracks unresolved requires' do doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to eq(['not_a_gem'] + doc_map.environ.requires) + expect(doc_map.unresolved_requires).to eq(['not_a_gem']) end it 'tracks uncached_gemspecs' do From 24775984e2a5df55bdd37956bbe3cc4520bb7b02 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 07:32:00 -0400 Subject: [PATCH 092/116] Fix merge issue --- lib/solargraph/pin/method.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 725bdecd1..7fc343630 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -645,8 +645,6 @@ def concat_example_tags protected - attr_writer :signatures - attr_writer :return_type end end From 3bfad8b00f410e72e1e75ebd61d2f965ec44b0e6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 07:37:47 -0400 Subject: [PATCH 093/116] Fix merge issue --- lib/solargraph/rbs_map/conversions.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 1ac7343ba..cd1bf320a 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -511,9 +511,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, @@ -527,7 +528,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)) From cff157fab8f6180142d7b020e68823d922d1c731 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 24 Jun 2025 07:55:12 -0400 Subject: [PATCH 094/116] Remove debug output Co-authored-by: Vince Broz --- lib/solargraph/doc_map.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 1843f5596..ded7bc7ec 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -225,7 +225,6 @@ def gemspecs_required_from_bundler def gemspecs_required_from_external_bundle logger.info 'Fetching gemspecs required from external bundle' - puts workspace.inspect return [] unless workspace&.directory Solargraph.with_clean_env do From 2fd785d9922ce3804d314af7de6a16206d2e2680 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 08:19:53 -0400 Subject: [PATCH 095/116] Bump version --- lib/solargraph/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index 09fdb0d4e..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.2' + VERSION = '0.56.alpha' end From dea8d368f2161d1ba920c048f6d3589ef9da86f7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 10:02:52 -0400 Subject: [PATCH 096/116] Fix merge issue --- lib/solargraph/gem_pins.rb | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index a69fe4f72..402e5235e 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -23,18 +23,6 @@ def self.build_yard_pins(gemspec) YardMap::Mapper.new(yardoc, gemspec).map end - # Build an array of pins from a gem specification. The process starts with - # YARD, enhances the resulting pins with RBS definitions, and appends RBS - # pins that don't exist in the YARD mapping. - # - # @param gemspec [Gem::Specification] - # @return [Array] - def self.build(gemspec) - yard_pins = build_yard_pins(gemspec) - rbs_map = RbsMap.from_gemspec(gemspec) - combine yard_pins, rbs_map - end - def self.combine_method_pins_by_path(pins) # @type [Hash{::Array(String, String) => ::Array}] by_name_and_class = {} @@ -60,11 +48,11 @@ 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 - rbs_api_map = Solargraph::ApiMap.new(pins: rbs_map.pins) + rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins) combined = yard_pins.map do |yard_pin| next yard_pin unless yard_pin.class == Pin::Method @@ -80,7 +68,7 @@ def self.combine(yard_pins, rbs_pins) logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" } out end - in_rbs_only = rbs_map.pins.select do |pin| + in_rbs_only = rbs_pins.select do |pin| pin.path.nil? || !in_yard.include?(pin.path) end combined + in_rbs_only From b2fca46ceacacc8191b497a1e25a71555f0b388e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 10:52:37 -0400 Subject: [PATCH 097/116] Handle cases related to future RBS collection pins --- lib/solargraph/pin/base.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 8b58c29db..b0af42385 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -219,6 +219,7 @@ def rbs_location? # @param other [self] # @return [void] def assert_same_macros(other) + return unless self.source == :yardoc && other.source == :yardoc assert_same_count(other, :macros) assert_same_array_content(other, :macros) { |macro| macro.tag.name } end @@ -293,6 +294,11 @@ def choose_pin_attr_with_same_name(other, attr) Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end + if val1.class != val2.class + Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym, + "Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") + return val1 + end [val1, val2].compact.min end From e87087cbd8ecf296371bc7ebfdb138b8f4a8db38 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 11:10:57 -0400 Subject: [PATCH 098/116] Adjust tests to match code --- spec/pin/base_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index 64e5b808c..6bcce6cef 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -4,9 +4,9 @@ it "will not combine pins with directive changes" do pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: 'A Foo class', - closure: Solargraph::Pin::ROOT_PIN) + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', - closure: Solargraph::Pin::ROOT_PIN) + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) # enable asserts with_env_var('SOLARGRAPH_ASSERTS', 'on') do @@ -16,9 +16,9 @@ it "will not combine pins with different directives" do pin1 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro my_macro', - closure: Solargraph::Pin::ROOT_PIN) + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) pin2 = Solargraph::Pin::Base.new(location: zero_location, name: 'Foo', comments: '@!macro other', - closure: Solargraph::Pin::ROOT_PIN) + source: :yardoc, closure: Solargraph::Pin::ROOT_PIN) expect(pin1.nearly?(pin2)).to be(false) with_env_var('SOLARGRAPH_ASSERTS', 'on') do expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :macros values/) From 71ec0d8f1852d8f1fcd6a6ed298a4ae9aa998cba Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 11:33:37 -0400 Subject: [PATCH 099/116] Drop assert exclusions --- lib/solargraph.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index df5dc7f8e..2b975a66c 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -55,10 +55,6 @@ class InvalidRubocopVersionError < RuntimeError; end # @param type [Symbol] Type of assert. def self.asserts_on?(type) - # Pending https://github.com/castwide/solargraph/pull/950 - return false if type == :combine_with_visibility - # Pending https://github.com/castwide/solargraph/pull/947 - return false if type == :combine_with_closure_name if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty? false elsif ENV['SOLARGRAPH_ASSERTS'] == 'on' From be7b37418c80926f8e6c3ab7bee2c29b3b29123d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 11:47:01 -0400 Subject: [PATCH 100/116] Reenable test --- spec/pin/local_variable_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 0e9e81871..88075efb9 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -30,8 +30,7 @@ class Foo # should indicate which one should override in the range situation end - # Pending https://github.com/castwide/solargraph/pull/947 - xit "asserts on attempt to merge namespace changes" do + it "asserts on attempt to merge namespace changes" do map1 = Solargraph::SourceMap.load_string(%( class Foo foo = 'foo' From 66a6bc3d8ebba8fb7e1fc936fa29be0d3fd66059 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 17:07:30 -0400 Subject: [PATCH 101/116] Efficiency fixes --- lib/solargraph/gem_pins.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 487b93e0f..d24ace8e4 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -23,24 +23,26 @@ def self.build(gemspec) combine yard_pins, rbs_map end + # @param pins [Array] def self.combine_method_pins_by_path(pins) - # @type [Hash{::Array(String, String) => ::Array}] - by_name_and_class = {} - pins.each do |pin| - by_name_and_class[[pin.name, pin.path, pin.class]] ||= [] - by_name_and_class[[pin.name, pin.path, pin.class]].push pin + # bad_pins = pins.select { |pin| pin.is_a?(Pin::Method) && pin.path == 'StringIO.open' && pin.source == :rbs }; raise "wtf: #{bad_pins}" if bad_pins.length > 1 + method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } + by_path = method_pins.group_by(&:path) + by_path.transform_values! do |pins| + GemPins.combine_method_pins(*pins) end - by_name_and_class.transform_values! do |pins| - method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } - [GemPins.combine_method_pins(*method_pins)].compact + alias_pins - end - by_name_and_class.values.flatten(1) + by_path.values + alias_pins end def self.combine_method_pins(*pins) out = pins.reduce(nil) do |memo, pin| - raise "sent wrong type of pin: #{pin.inspect}" unless pin.class == Pin::Method next pin if memo.nil? + if memo == pin && memo.source != :combined + # @todo we should track down situations where we are handled + # the same pin from the same source here and eliminate them - + # this is an efficiency workaround for now + next memo + end memo.combine_with(pin) end logger.debug { "GemPins.combine_method_pins(pins.length=#{pins.length}, pins=#{pins}) => #{out.inspect}" } From 2036a7edee0de53bb90ddc088825a3311b9a1b59 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 24 Jun 2025 17:31:38 -0400 Subject: [PATCH 102/116] Fix broken <=> approach --- lib/solargraph/equality.rb | 5 ----- lib/solargraph/location.rb | 9 +++++++++ lib/solargraph/pin/base.rb | 18 ++++++++++-------- lib/solargraph/pin/callable.rb | 2 +- lib/solargraph/position.rb | 9 +++++++++ lib/solargraph/range.rb | 9 +++++++++ 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index 8c5611627..0667efacd 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -29,10 +29,5 @@ def freeze equality_fields.each(&:freeze) super end - - def <=>(other) - return nil unless other.is_a?(self.class) - equality_fields <=> other.equality_fields - end end end diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 909e19eaa..74d1318df 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -25,6 +25,15 @@ def initialize filename, range [filename, range] end + def <=>(other) + return nil unless other.is_a?(Location) + if filename == other.filename + range <=> other.range + else + filename <=> other.filename + end + end + def rbs? filename.end_with?('.rbs') end diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index b0af42385..49e066b2c 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -163,13 +163,6 @@ def dodgy_return_type_source? location&.filename&.include?('core_ext/object/') end - # @param p1 [self] - def <=>(p1) - return nil unless p1.is_a?(self.class) - return 0 if self == p1 - equality_fields <=> equality_fields - end - # when choices are arbitrary, make sure the choice is consistent # # @param other [Pin::Base] @@ -294,12 +287,21 @@ def choose_pin_attr_with_same_name(other, attr) Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym, "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") end + choose_pin_attr(other, attr) + end + + def choose_pin_attr(other, attr) + # @type [Pin::Base, nil] + val1 = send(attr) + # @type [Pin::Base, nil] + val2 = other.send(attr) if val1.class != val2.class Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym, "Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}") return val1 end - [val1, val2].compact.min + # arbitrary way of choosing a pin + [val1, val2].compact.min_by { _1.best_location.to_s } end def assert_source_provided diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index e97fe83a5..20d2301eb 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -31,7 +31,7 @@ def combine_blocks(other) elsif other.block.nil? block else - choose(other, :block) + choose_pin_attr(other, :block) end end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 3cf0d229e..1bd31e0f5 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -26,6 +26,15 @@ def initialize line, character [line, character] end + def <=>(other) + return nil unless other.is_a?(Position) + if line == other.line + character <=> other.character + else + line <=> other.line + end + end + # Get a hash of the position. This representation is suitable for use in # the language server protocol. # diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index d34655848..615f180af 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -24,6 +24,15 @@ def initialize start, ending [start, ending] end + def <=>(other) + return nil unless other.is_a?(Range) + if start == other.start + ending <=> other.ending + else + start <=> other.start + end + end + # Get a hash of the range. This representation is suitable for use in # the language server protocol. # From 16c76ddb35f2412fbaafba1a5ce056dc7eb60e3e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 12:58:18 -0400 Subject: [PATCH 103/116] Map RBS 'untyped' type (RBS::Types::Bases::Any) to 'undefined' This seems to be the most clear mapping - viewing these three names as all meaning 'I am making no promises on what type this can be'. https://github.com/ruby/rbs/blob/master/docs/syntax.md#base-types 'untyped' is used in RBS as the initial type inserted for all arguments and return values when stubs are generated Also: * Refactor RbsMap::Conversions into a standalone class for testing purposes --- lib/solargraph/rbs_map.rb | 16 ++++++-- lib/solargraph/rbs_map/conversions.rb | 17 ++++++--- lib/solargraph/rbs_map/core_map.rb | 18 +++++++-- spec/rbs_map/conversions_spec.rb | 54 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 spec/rbs_map/conversions_spec.rb diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 52502facc..b0b4b470d 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -10,7 +10,7 @@ class RbsMap autoload :CoreFills, 'solargraph/rbs_map/core_fills' autoload :StdlibMap, 'solargraph/rbs_map/stdlib_map' - include Conversions + include Logging # @type [Hash{String => RbsMap}] @@rbs_maps_hash = {} @@ -25,10 +25,12 @@ def initialize library, version = nil, directories: [] @version = version @collection = nil @directories = directories - loader = RBS::EnvironmentLoader.new(core_root: nil, repository: repository) add_library loader, library, version return unless resolved? - load_environment_to_pins(loader) + end + + def pins + @pins ||= resolved? ? conversions.pins : [] end # @generic T @@ -71,6 +73,14 @@ def self.from_gemspec(gemspec) private + def loader + @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) + end + + def conversions + @conversions ||= Conversions.new(loader: loader) + end + # @param loader [RBS::EnvironmentLoader] # @param library [String] # @return [Boolean] true if adding the library succeeded diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 11a2ae39b..972690326 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -6,7 +6,7 @@ module Solargraph class RbsMap # Functions for converting RBS declarations to Solargraph pins # - module Conversions + class Conversions include Logging # A container for tracking the current context of the RBS conversion @@ -22,11 +22,17 @@ def initialize visibility = :public end end - # @return [Array] - def pins - @pins ||= [] + def initialize(loader:) + @loader = loader + @pins = [] + load_environment_to_pins(loader) end + attr_reader :loader + + # @return [Array] + attr_reader :pins + private # @return [Hash{String => RBS::AST::Declarations::TypeAlias}] @@ -683,8 +689,7 @@ def other_type_to_tag type if type.is_a?(RBS::Types::Optional) "#{other_type_to_tag(type.type)}, nil" elsif type.is_a?(RBS::Types::Bases::Any) - # @todo Not sure what to do with Any yet - 'BasicObject' + 'undefined' elsif type.is_a?(RBS::Types::Bases::Bool) 'Boolean' elsif type.is_a?(RBS::Types::Tuple) diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 1fac3e8d7..fb73fda3d 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -5,7 +5,6 @@ class RbsMap # Ruby core pins # class CoreMap - include Conversions FILLS_DIRECTORY = File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills') @@ -14,16 +13,29 @@ def initialize if cache pins.replace cache else - loader = RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) loader.add(path: Pathname(FILLS_DIRECTORY)) - load_environment_to_pins(loader) pins.concat RbsMap::CoreFills::ALL + @pins = conversions.pins processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } pins.replace processed Cache.save('core.ser', pins) end end + + def pins + @pins ||= [] + end + + private + + def loader + @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) + end + + def conversions + @conversions ||= Conversions.new(loader: loader) + end end end end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb new file mode 100644 index 000000000..09c203687 --- /dev/null +++ b/spec/rbs_map/conversions_spec.rb @@ -0,0 +1,54 @@ +describe Solargraph::RbsMap::Conversions do + # create a temporary directory with the scope of the spec + around do |example| + require 'tmpdir' + Dir.mktmpdir("rspec-solargraph-") do |dir| + @temp_dir = dir + example.run + end + end + + let(:rbs_repo) do + RBS::Repository.new(no_stdlib: false) + end + + let(:loader) do + RBS::EnvironmentLoader.new(core_root: nil, repository: rbs_repo) + end + + let(:conversions) do + Solargraph::RbsMap::Conversions.new(loader: loader) + end + + let(:pins) do + conversions.pins + end + + before do + rbs_file = File.join(temp_dir, 'foo.rbs') + File.write(rbs_file, rbs) + loader.add(path: Pathname(temp_dir)) + end + + attr_reader :temp_dir + + context 'with untyped response' do + let(:rbs) do + <<~RBS + class Foo + def bar: () -> untyped + end + RBS + end + + subject(:method_pin) { pins.find { |pin| pin.path == 'Foo#bar' } } + + it { should_not be_nil } + + it { should be_a(Solargraph::Pin::Method) } + + it 'maps untyped in RBS to undefined in Solargraph 'do + expect(method_pin.return_type.tag).to eq('undefined') + end + end +end From 3da027f08ee1dbdb7ee4d65270109c7ea0fa981c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 13:26:18 -0400 Subject: [PATCH 104/116] Fix specs --- lib/solargraph/rbs_map/core_map.rb | 2 +- spec/source_map/clip_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index fb73fda3d..b11cea3e7 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -14,8 +14,8 @@ def initialize pins.replace cache else loader.add(path: Pathname(FILLS_DIRECTORY)) - pins.concat RbsMap::CoreFills::ALL @pins = conversions.pins + @pins.concat RbsMap::CoreFills::ALL processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } pins.replace processed diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 09b8c76bd..d32b8b76d 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -746,7 +746,7 @@ def self.new expect(clip.infer.tag).to eq('Class') end - it 'infers BasicObject from Class#new' do + it 'infers undefined from Class#new' do source = Solargraph::Source.load_string(%( cls = Class.new cls.new @@ -754,17 +754,17 @@ def self.new api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [2, 11]) - expect(clip.infer.tag).to eq('BasicObject') + expect(clip.infer.tag).to eq('undefined') end - it 'infers BasicObject from Class.new.new' do + it 'infers undefined from Class.new.new' do source = Solargraph::Source.load_string(%( Class.new.new ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [1, 17]) - expect(clip.infer.tag).to eq('BasicObject') + expect(clip.infer.tag).to eq('undefined') end it 'completes class instance variables in the namespace' do From db5c332f36ad9d0c00e5d291e2a65fc61664625b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 15:08:20 -0400 Subject: [PATCH 105/116] Reduce logging volume --- lib/solargraph/doc_map.rb | 2 +- lib/solargraph/rbs_map/conversions.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 202eae0dc..cbcd63f0e 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -231,7 +231,7 @@ def deserialize_stdlib_rbs_map path if map.resolved? logger.debug { "Loading stdlib pins for #{path}" } @pins.concat map.pins - logger.info { "Loaded #{map.pins.length} stdlib pins for #{path}" } + logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" } map.pins else # @todo Temporarily ignoring unresolved `require 'set'` diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 95f6cf97d..490ac5bea 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -46,7 +46,7 @@ def load_environment_to_pins(loader) environment = RBS::Environment.from_loader(loader).resolve_type_names cursor = pins.length if environment.declarations.empty? - Solargraph.logger.warn "No RBS declarations found in environment for #{loader.core_root}." + Solargraph.logger.info "No RBS declarations found in environment for core_root #{loader.core_root.inspect}, libraries #{loader.libs} and directories #{loader.dirs}" return end environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) } From 93bbb5d515d4450d3c831ee9ef3044cc0c29619c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 15:30:44 -0400 Subject: [PATCH 106/116] Fix stdlib/core paths --- lib/solargraph/pin_cache.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 849adcaf1..9013dd0d9 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -30,8 +30,12 @@ def yardoc_path gemspec File.join(base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc") end + def stdlib_path + File.join(work_dir, 'stdlib') + end + def stdlib_require_path require - File.join(work_dir, 'stdlib', "#{require}.ser") + File.join(stdlib_path, "#{require}.ser") end def deserialize_stdlib_require require @@ -107,11 +111,11 @@ def has_rbs_collection?(gemspec, hash) end def uncache_core - uncache("core.ser") + uncache(core_path) end def uncache_stdlib - uncache("stdlib") + uncache(stdlib_path) end def uncache_gem(gemspec, out: nil) From f36a69bc945d0583248f3a65c4403270d8a41a86 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 15:40:20 -0400 Subject: [PATCH 107/116] Undo mistaken refactor --- lib/solargraph/rbs_map/stdlib_map.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index d40d041cb..6e6cc259d 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -14,25 +14,21 @@ class StdlibMap < RbsMap # @param library [String] def initialize library - super - end - - def gems - return @pins if @pins cached_pins = PinCache.deserialize_stdlib_require library if cached_pins @pins = cached_pins @resolved = true - logger.warn { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } - cached_pins + logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } else - generated_pins = load_environment_to_pins(loader) + super unless resolved? - logger.warn { "Could not resolve #{library.inspect}" } - return [] + @pins = [] + logger.info { "Could not resolve #{library.inspect}" } + return end + generated_pins = pins + logger.debug { "Found #{generated_pins.length} pins for stdlib library #{library}" } PinCache.serialize_stdlib_require library, generated_pins - @pins = generated_pins end end From 2e135ae0d881ac975c047927711a046131330b51 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 15:58:53 -0400 Subject: [PATCH 108/116] Undo mistaken refactor --- lib/solargraph/rbs_map/stdlib_map.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index 6e6cc259d..87f00dba3 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -16,8 +16,9 @@ class StdlibMap < RbsMap def initialize library cached_pins = PinCache.deserialize_stdlib_require library if cached_pins - @pins = cached_pins + @pins = cache @resolved = true + @loaded = true logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } else super From 46ad545d0b7a23c0fe1c09db04f7a1127ea34992 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 16:03:33 -0400 Subject: [PATCH 109/116] Fix merge issue --- lib/solargraph/rbs_map/stdlib_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index 87f00dba3..b6804157f 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -16,7 +16,7 @@ class StdlibMap < RbsMap def initialize library cached_pins = PinCache.deserialize_stdlib_require library if cached_pins - @pins = cache + @pins = cached_pins @resolved = true @loaded = true logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } From 10453e3e814bd8b7f8db608e3f2258298b576509 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 16:10:47 -0400 Subject: [PATCH 110/116] Fix merge issue --- lib/solargraph/rbs_map/stdlib_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index 87f00dba3..b6804157f 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -16,7 +16,7 @@ class StdlibMap < RbsMap def initialize library cached_pins = PinCache.deserialize_stdlib_require library if cached_pins - @pins = cache + @pins = cached_pins @resolved = true @loaded = true logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } From fc652934d557b29cf638b045e69db7905c4cc46f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 25 Jun 2025 18:01:32 -0400 Subject: [PATCH 111/116] Support ActiveSupport::Concern pattern for class methods --- .yardopts | 1 + lib/solargraph/api_map.rb | 20 ++++++++++++++++++++ lib/solargraph/pin/method.rb | 8 +++++++- lib/solargraph/yardoc.rb | 2 +- solargraph.gemspec | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) 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/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index c412fb4cb..1f2f57bc8 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -723,6 +723,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 @@ -735,6 +737,24 @@ 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| + logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + rooted_include_tag = qualify(include_tag, rooted_tag) + + # ActiveSupport::Concern is syntactic sugar for a common + # pattern to provide virtual class method - i.e., if Foo + # includes Bar and Bar is a module using this + # pattern, Bar can supply class methods which will also + # appear under Foo. + + # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html + included_class_pins = inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) + # activesupport_concern_pins = included_class_pins.select { |p| p.virtual_class_method? } + # result.concat activesupport_concern_pins + result.concat included_class_pins # TODO remove this line once we have activesupport::concern support + 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? diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 5d50ae0b4..1833626de 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -12,6 +12,10 @@ class Method < Callable attr_writer :signatures + def virtual_class_method? + @virtual_class_method + end + # @return [Parser::AST::Node] attr_reader :node @@ -22,7 +26,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, + virtual_class_method: false, **splat super(**splat) @visibility = visibility @explicit = explicit @@ -31,6 +36,7 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @attribute = attribute @signatures = signatures @anon_splat = anon_splat + @virtual_class_method = virtual_class_method end # @return [Array] 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' From d17745382696033191b81e518966e8074382b8a6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 10:50:06 -0400 Subject: [PATCH 112/116] Add ::ClassMethods support --- lib/solargraph/api_map.rb | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 1f2f57bc8..66ae62382 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -738,20 +738,26 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end else store.get_includes(fqns).reverse.each do |include_tag| - logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{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(include_tag) # ActiveSupport::Concern is syntactic sugar for a common - # pattern to provide virtual class method - i.e., if Foo - # includes Bar and Bar is a module using this - # pattern, Bar can supply class methods which will also - # appear under Foo. + # pattern to include class methods while mixing-in a Module # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html - included_class_pins = inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) - # activesupport_concern_pins = included_class_pins.select { |p| p.virtual_class_method? } - # result.concat activesupport_concern_pins - result.concat included_class_pins # TODO remove this line once we have activesupport::concern support + logger.debug { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}) - Handling class include include_tag=#{include_tag}" } + if module_extends.include? 'ActiveSupport::Concern' + rooted_include_tag = qualify(include_tag, rooted_tag) + 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 + included_classmethods_pins = inner_get_methods_from_reference(rooted_include_tag + "::ClassMethods", 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}" } From 6adb558096c4cd2b54091140bbaedfe06b623719 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 26 Jun 2025 07:33:41 -0400 Subject: [PATCH 113/116] Differentiate cache based on YARD plugin used --- lib/solargraph/yardoc.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 858c7774a..287eb9809 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'yard' +require 'yard-activesupport-concern' + module Solargraph # Methods for caching and loading YARD documentation for gems. # @@ -35,7 +38,10 @@ def cached?(gemspec) # @param gemspec [Gem::Specification] # @return [String] def path_for(gemspec) - File.join(Solargraph::Cache.base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc") + File.join(Solargraph::Cache.base_dir, + "yard-#{YARD::VERSION}", + "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}", + "#{gemspec.name}-#{gemspec.version}.yardoc") end # Load a gem's yardoc and return its code objects. From 9b7a76ea2ee7d5ca78f5cecc68c271366217f412 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 26 Jun 2025 07:41:57 -0400 Subject: [PATCH 114/116] Fix merge --- lib/solargraph/pin/method.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 3ddf35e77..60ef6fb0c 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -12,10 +12,6 @@ class Method < Callable attr_writer :signatures - def virtual_class_method? - @virtual_class_method - end - # @return [Parser::AST::Node] attr_reader :node @@ -36,7 +32,6 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @attribute = attribute @signatures = signatures @anon_splat = anon_splat - @virtual_class_method = virtual_class_method end # @return [Array] From 5505e3769bd6d765756ab1a2d674baeeb5f59778 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 26 Jun 2025 09:25:37 -0400 Subject: [PATCH 115/116] Fix issue with rooting a namespace --- lib/solargraph/api_map.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 22c00f039..8d3402e34 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -685,15 +685,15 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false 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(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' - rooted_include_tag = qualify(include_tag, rooted_tag) unless rooted_include_tag.nil? # yard-activesupport-concern pulls methods inside # 'class_methods' blocks into main class visible from YARD @@ -701,7 +701,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false result.concat included_class_pins # another pattern is to put class methods inside a submodule - included_classmethods_pins = inner_get_methods_from_reference(rooted_include_tag + "::ClassMethods", namespace_pin, rooted_type, :instance, visibility, deep, skip, true) + 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 From 339356fbc739bfdc76cfd5fdfb624388800a0fbf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 29 Jun 2025 12:05:46 -0400 Subject: [PATCH 116/116] [regression] Gem caching perf and logging fixes * [regression] Only log when we are actually doing work caching pins * [regression] Fix misspelled mutex-y variable name in gem caching - probably fixes #976 * [regression] Speed up 'solargraph gems' caching by only loading api_map once, avoiding extra loading of files from disk * [regression] Use existing pattern to keep only one cache of combined pins in memory, instead of one per DocMap (~0.25s speed-up in solargraph project per docmap on my computer, ymmv) --- lib/solargraph/doc_map.rb | 19 +++++++++++++++---- lib/solargraph/library.rb | 2 +- lib/solargraph/shell.rb | 10 ++++++---- 3 files changed, 22 insertions(+), 9 deletions(-) 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/library.rb b/lib/solargraph/library.rb index c2875011d..1ed991481 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -587,7 +587,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/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)