From 10ef2a80252eacd1c49c47c482a074ac918cd6d4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 20 Mar 2025 16:56:05 -0400 Subject: [PATCH 001/524] At strong level, don't allow method calls off of nilable types More specifically, require that all unique types of a complex type offer the same method before it can match. --- lib/solargraph/api_map.rb | 24 +++++++++++++++++----- lib/solargraph/pin/parameter.rb | 8 ++++++++ lib/solargraph/shell.rb | 6 ++++-- lib/solargraph/source/chain/call.rb | 20 +++++++++++-------- lib/solargraph/type_checker.rb | 18 ++++++++++------- lib/solargraph/type_checker/rules.rb | 4 ++++ spec/source/chain/call_spec.rb | 30 ++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 22 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 53c4f1814..ee0939fbe 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -25,13 +25,27 @@ class ApiMap attr_reader :missing_docs # @param pins [Array] - def initialize pins: [] + # @param loose_unions [Boolean] if true, a potential type can be + # inferred if ANY of the UniqueTypes in the base chain's + # ComplexType match it. If false, every single UniqueTypes in + # the base must be ALL able to independently provide this + # type. The former is useful during completion, but the + # latter is best for typechecking at higher levels. + # + # Currently applies only to selecting potential methods to + # select in a Call link, but is likely to expand in the + # future to similar situations. + # + def initialize pins: [], loose_unions: true @source_map_hash = {} @cache = Cache.new @method_alias_stack = [] + @loose_unions = loose_unions index pins end + attr_reader :loose_unions + # @param pins [Array] # @return [self] def index pins @@ -124,8 +138,8 @@ def clip_at filename, position # # @param directory [String] # @return [ApiMap] - def self.load directory - api_map = new + def self.load directory, loose_unions: true + api_map = new(loose_unions: loose_unions) workspace = Solargraph::Workspace.new(directory) # api_map.catalog Bench.new(workspace: workspace) library = Library.new(workspace) @@ -139,8 +153,8 @@ def self.load directory # # @param directory [String] # @return [ApiMap] - def self.load_with_cache directory - api_map = load(directory) + def self.load_with_cache directory, loose_unions: true + api_map = load(directory, loose_unions: loose_unions) return api_map if api_map.uncached_gemspecs.empty? api_map.uncached_gemspecs.each do |gemspec| diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index b56aa388e..1ce8647c8 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -28,6 +28,14 @@ def restarg? decl == :restarg end + def mandatory_positional? + decl == :arg + end + + def positional? + !keyword? + end + def rest? decl == :restarg || decl == :kwrestarg end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index bb4e9fef3..fd552bd8b 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -156,7 +156,9 @@ def reporters # @return [void] def typecheck *files directory = File.realpath(options[:directory]) - api_map = Solargraph::ApiMap.load_with_cache(directory) + level = options[:level].to_sym + rules = Solargraph::TypeChecker::Rules.new(level) + api_map = Solargraph::ApiMap.load_with_cache(directory, loose_unions: rules.loose_unions?) if files.empty? files = api_map.source_maps.map(&:filename) else @@ -165,7 +167,7 @@ def typecheck *files probcount = 0 filecount = 0 files.each do |file| - checker = TypeChecker.new(file, api_map: api_map, level: options[:level].to_sym) + checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: level) problems = checker.problems next if problems.empty? problems.sort! { |a, b| a.location.range.start.line <=> b.location.range.start.line } diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 3c2d4ed73..e901fb957 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -36,9 +36,16 @@ def resolve api_map, name_pin, locals [] end return inferred_pins(found, api_map, name_pin.context, locals) unless found.empty? - # @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) + if api_map.loose_unions + # fetch methods which ANY of the potential context types provide + pins = name_pin.binder.each_unique_type.flat_map do |context| + api_map.get_method_stack(context.namespace == '' ? '' : context.to_s, word, scope: context.scope) + end + else + # grab pins which are provided by every potential context type + pins = name_pin.binder.each_unique_type.map do |context| + api_map.get_method_stack(context.namespace == '' ? '' : context.to_s, word, scope: context.scope) + end.reduce(:&) end return [] if pins.empty? inferred_pins(pins, api_map, name_pin.context, locals) @@ -200,12 +207,9 @@ def extra_return_type docstring, context # @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?) + mandatory_positional_param_count = signature.parameters.count(&:mandatory_positional?) + return false if arguments.count < mandatory_positional_param_count true end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 03644f7f0..9f4303f87 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -24,11 +24,13 @@ class TypeChecker # @param filename [String] # @param api_map [ApiMap, nil] # @param level [Symbol] - def initialize filename, api_map: nil, level: :normal + def initialize filename, api_map: nil, level: :normal, rules: Rules.new(level) @filename = filename # @todo Smarter directory resolution - @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename)) - @rules = Rules.new(level) + @rules = rules + @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename), + loose_unions: rules.loose_unions?) + @marked_ranges = [] end @@ -55,9 +57,10 @@ class << self # @return [self] def load filename, level = :normal source = Solargraph::Source.load(filename) - api_map = Solargraph::ApiMap.new + rules = Rules.new(level) + api_map = Solargraph::ApiMap.new(loose_unions: rules.loose_unions?) api_map.map(source) - new(filename, api_map: api_map, level: level) + new(filename, api_map: api_map, level: level, rules: rules) end # @param code [String] @@ -66,9 +69,10 @@ def load filename, level = :normal # @return [self] def load_string code, filename = nil, level = :normal source = Solargraph::Source.load_string(code, filename) - api_map = Solargraph::ApiMap.new + rules = Rules.new(level) + api_map = Solargraph::ApiMap.new(loose_unions: rules.loose_unions?) api_map.map(source) - new(filename, api_map: api_map, level: level) + new(filename, api_map: api_map, level: level, rules: rules) end end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 84eb369bb..76aa7b8dd 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -49,6 +49,10 @@ def must_tag_or_infer? rank > LEVELS[:typed] end + def loose_unions? + rank < LEVELS[:strong] + end + def validate_tags? rank > LEVELS[:normal] end diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 74b5139f6..a4dbde0fb 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -307,4 +307,34 @@ def baz # @todo It would be more accurate to return `Enumerator>` here expect(type.tag).to eq('Enumerator>') end + + it 'allows calls off of nilable objects by default' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 6)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + expect(type.tag).to eq('String') + end + + it 'denies calls off of nilable objects when loose union mode is off' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new(loose_unions: false) + api_map.map source + + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 6)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + expect(type.tag).to eq('undefined') + end end From 16b4a0d2700d293b4dc23bd4631f80b13daa9190 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 22 May 2025 14:20:29 -0400 Subject: [PATCH 002/524] Add assertions around method aliases --- lib/solargraph.rb | 19 +++++++++++++++++++ lib/solargraph/api_map.rb | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) 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/api_map.rb b/lib/solargraph/api_map.rb index 9443e8529..b0225305e 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -808,7 +808,10 @@ def prefer_non_nil_variables pins 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) + if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) + Solargraph.assert_or_log(:alias_visibility) { "Rejecting alias - visibility of target is #{resolved.visibility}, looking for visibility #{visibility}" } + next pin + end resolved end.compact end @@ -821,7 +824,10 @@ def resolve_method_alias pin @method_alias_stack.push pin.path origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope).first @method_alias_stack.pop - return nil if origin.nil? + if origin.nil? + Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing = #{pin.inspect}" } + return nil + end args = { location: pin.location, closure: pin.closure, From 5bc55d306911fb2f40e9b56699264dc76aec1d7e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 19:31:10 -0400 Subject: [PATCH 003/524] Fix merge --- lib/solargraph/source/chain/call.rb | 2 +- spec/source/chain/call_spec.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 3e975c9a1..18278e0fe 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -58,7 +58,7 @@ def resolve api_map, name_pin, locals return inferred_pins(found, api_map, name_pin, locals) unless found.empty? if api_map.loose_unions # fetch methods which ANY of the potential context types provide - pins = name_pin.binder.each_unique_type.map do |context| + pins = name_pin.binder.each_unique_type.flat_map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) [stack.first].compact diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 32c5a172d..ab22eca44 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -657,6 +657,5 @@ def bl clip = api_map.clip_at('test.rb', [3, 8]) expect(clip.infer.rooted_tags).to eq('::String') ->>>>>>> origin/master end end From 65e3c0b4309271c6a962dff123f1d77632f93e36 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 9 Jul 2025 16:00:55 -0400 Subject: [PATCH 004/524] Suppress some known problematic aliases --- lib/solargraph.rb | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 2b975a66c..4a674877c 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -54,19 +54,28 @@ class InvalidRubocopVersionError < RuntimeError; end VIEWS_PATH = File.join(dir, 'solargraph', 'views') # @param type [Symbol] Type of assert. - 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 + def self.asserts_on? + @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 = nil, &block) - raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type) + if asserts_on? + msg ||= block.call + + # not ready for prime time + return if [:combine_with_visibility].include?(type) + # conditional aliases to handle compatibility corner cases + return if type == :alias_target_missing && msg.include?('highline/compatibility.rb') + return if type == :alias_target_missing && msg.include?('lib/json/add/date.rb') + raise msg + end logger.info msg, &block end From 336305f1d4b5eb88524dc3a4a85f445ab22ad03f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 9 Jul 2025 16:03:01 -0400 Subject: [PATCH 005/524] Drop unintended change --- lib/solargraph.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 4a674877c..8295945cd 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -53,16 +53,15 @@ class InvalidRubocopVersionError < RuntimeError; end dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') - # @param type [Symbol] Type of assert. def self.asserts_on? - @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) From c7079d50c3aad7b69a83496a2a25f3da1a7e6e8c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 9 Jul 2025 16:06:03 -0400 Subject: [PATCH 006/524] Update spec --- spec/pin/local_variable_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 88075efb9..369a58bc4 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -46,7 +46,7 @@ class Bar # set env variable 'FOO' to 'true' in block with_env_var('SOLARGRAPH_ASSERTS', 'on') do - expect(Solargraph.asserts_on?(:combine_with_closure_name)).to be true + expect(Solargraph.asserts_on?).to be true expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) end end From 9a2db0c0c053b72fa4bf7da239b9d1dc20b08242 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 11 Jul 2025 17:22:07 -0400 Subject: [PATCH 007/524] Fix type issues --- lib/solargraph.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 8295945cd..f5348ded6 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -66,8 +66,11 @@ def self.asserts_on? def self.assert_or_log(type, msg = nil, &block) if asserts_on? + # @type [String, nil] msg ||= block.call + raise "No message given for #{type.inspect}" if msg.nil? + # not ready for prime time return if [:combine_with_visibility].include?(type) # conditional aliases to handle compatibility corner cases From 8b1107501abeb326df1f8fecf3030f8f4b632b9e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 12:44:19 -0400 Subject: [PATCH 008/524] Add 'solargraph method_pin' command for debugging ```sh $ SOLARGRAPH_ASSERTS=on bundle exec solargraph method_pin --rbs 'RuboCop::AST::ArrayNode#values' def values: () -> Array $ bundle exec solargraph help method_pin Usage: solargraph method_pin [PATH] Options: [--rbs], [--no-rbs], [--skip-rbs] # Output the pin as RBS # Default: false [--typify], [--no-typify], [--skip-typify] # Output the calculated return type of the pin from annotations # Default: false [--probe], [--no-probe], [--skip-probe] # Output the calculated return type of the pin from annotations and inference # Default: false [--stack], [--no-stack], [--skip-stack] # Show entire stack by including definitions in superclasses # Default: false Describe a method pin $ ``` --- lib/solargraph/shell.rb | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 8f02f6ec9..938c31a11 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -239,6 +239,54 @@ def list puts "#{workspace.filenames.length} files total." end + desc 'method_pin [PATH]', 'Describe a method pin' + option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false + option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false + option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false + option :stack, type: :boolean, desc: 'Show entire stack by including definitions in superclasses', default: false + # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' + # @return [void] + def method_pin path + api_map = Solargraph::ApiMap.load_with_cache('.', STDERR) + + pins = if options[:stack] + scope, ns, meth = if path.include? '#' + [:instance, *path.split('#', 2)] + else + [:class, *path.split('.', 2)] + end + api_map.get_method_stack(ns, meth, scope: scope) + else + api_map.get_path_pins path + end + if pins.empty? + STDERR.puts "Pin not found for path '#{path}'" + exit 1 + end + pins.each do |pin| + if options[:typify] || options[:probe] + type = ComplexType::UNDEFINED + if options[:typify] + type = pin.typify(api_map) + end + if options[:probe] && type.undefined? + type = pin.probe(api_map) + end + if options[:rbs] + puts type.to_rbs + else + puts type.rooted_tag + end + else + if options[:rbs] + puts pin.to_rbs + else + puts pin.inspect + end + end + end + end + private # @param pin [Solargraph::Pin::Base] From ca88ad8c43ef25ed5323f90de6a9c3b3eab71d8b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 13:25:22 -0400 Subject: [PATCH 009/524] More logging --- 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 34ad11d1b..0c77ab06a 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -894,7 +894,7 @@ def resolve_method_alias pin origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first @method_alias_stack.pop if origin.nil? - Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing = #{pin.inspect}" } + Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{pin.full_context.tag} #{pin.original}() in #{pin.scope} scope = #{pin.inspect}" } return nil end args = { From da58721a8d80ec78416a037bf3133d832f5f001f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 13:25:45 -0400 Subject: [PATCH 010/524] Improve RBS definition for aliases --- lib/solargraph/pin/method_alias.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/solargraph/pin/method_alias.rb b/lib/solargraph/pin/method_alias.rb index 8636169a8..5428ec46e 100644 --- a/lib/solargraph/pin/method_alias.rb +++ b/lib/solargraph/pin/method_alias.rb @@ -23,6 +23,14 @@ def visibility :public end + def to_rbs + if scope == :class + rbs = "alias self.#{name} self.#{original}" + else + rbs = "alias #{name} #{original}" + end + end + def path @path ||= namespace + (scope == :instance ? '#' : '.') + name end From 6eccf31316c9692d8a22d7640bf14a0ac210a360 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 13:26:01 -0400 Subject: [PATCH 011/524] Avoid unnecessary alias assertions while combining pins --- lib/solargraph/gem_pins.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index d24ace8e4..08ac99da7 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -54,9 +54,10 @@ def self.combine_method_pins(*pins) # @return [Array] def self.combine(yard_pins, rbs_map) in_yard = Set.new - rbs_api_map = Solargraph::ApiMap.new(pins: rbs_map.pins) + rbs_store = Solargraph::ApiMap::Store.new(rbs_pins) combined = yard_pins.map do |yard_pin| - next yard_pin unless yard_pin.class == Pin::Method + rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first + next yard_pin unless rbs_pin && yard_pin.class == Pin::Method in_yard.add yard_pin.path From e3ecdc93f9e5d2b03255e0c39d32ae21e846965f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 13:28:01 -0400 Subject: [PATCH 012/524] Apply suggestion from @apiology --- lib/solargraph/gem_pins.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 3ee6c30b3..bc4ec9666 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -52,7 +52,7 @@ def self.combine(yard_pins, rbs_pins) in_yard = Set.new rbs_store = Solargraph::ApiMap::Store.new(rbs_pins) combined = yard_pins.map do |yard_pin| - in_yard.add yard_pin.path + in_yard.add yard_pin.path rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first next yard_pin unless rbs_pin && yard_pin.class == Pin::Method From 91defa6f65cf55f9052ca434cd0b0261c48a761e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 14:28:12 -0400 Subject: [PATCH 013/524] Fix RuboCop complaint --- lib/solargraph/pin/method_alias.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/method_alias.rb b/lib/solargraph/pin/method_alias.rb index 5428ec46e..8cc656d23 100644 --- a/lib/solargraph/pin/method_alias.rb +++ b/lib/solargraph/pin/method_alias.rb @@ -25,9 +25,9 @@ def visibility def to_rbs if scope == :class - rbs = "alias self.#{name} self.#{original}" + "alias self.#{name} self.#{original}" else - rbs = "alias #{name} #{original}" + "alias #{name} #{original}" end end From 2be3a790e50c11cf812bf00d60b34bff3297a7c7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 14:49:03 -0400 Subject: [PATCH 014/524] Clarify logic in yard/rbs merging --- lib/solargraph/gem_pins.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index bc4ec9666..9f965f6f8 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -30,6 +30,8 @@ def self.combine_method_pins_by_path(pins) by_path.values + alias_pins end + # @param pins [Pin::Method] + # @return [Pin::Base, nil] def self.combine_method_pins(*pins) out = pins.reduce(nil) do |memo, pin| next pin if memo.nil? @@ -54,13 +56,21 @@ def self.combine(yard_pins, rbs_pins) combined = yard_pins.map do |yard_pin| in_yard.add yard_pin.path rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first - next yard_pin unless rbs_pin && yard_pin.class == Pin::Method + + next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method) 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 + # at this point both yard_pins and rbs_pins are methods or + # method aliases. if not plain methods, prefer the YARD one + next yard_pin if rbs_pin.class != Pin::Method + + next rbs_pin if yard_pin.class != Pin::Method + + # both are method pins 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 From bf612959ec8b43b888dac18b4ee6823af5e6ffc5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 14 Jul 2025 07:27:58 -0400 Subject: [PATCH 015/524] RuboCop and Solargraph fixes --- lib/solargraph/shell.rb | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 938c31a11..153e77f0e 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -272,18 +272,11 @@ def method_pin path if options[:probe] && type.undefined? type = pin.probe(api_map) end - if options[:rbs] - puts type.to_rbs - else - puts type.rooted_tag - end - else - if options[:rbs] - puts pin.to_rbs - else - puts pin.inspect - end + print_type(type) + next end + + print_pin(pin) end end @@ -312,5 +305,25 @@ def do_cache gemspec, api_map # typecheck doesn't complain on the below line api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout) end + + # @param type [ComplexType] + # @return [void] + def print_type(type) + if options[:rbs] + puts type.to_rbs + else + puts type.rooted_tag + end + end + + # @param pin [Solargraph::Pin::Base] + # @return [void] + def print_pin(pin) + if options[:rbs] + puts pin.to_rbs + else + puts pin.inspect + end + end end end From 90e93c67f9d7de6ceb135b6827cc2b07bc72eff5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 18 Jul 2025 18:31:46 -0400 Subject: [PATCH 016/524] Improve typechecking of generics --- lib/solargraph/complex_type.rb | 57 +++++- lib/solargraph/complex_type/type_methods.rb | 9 + lib/solargraph/complex_type/unique_type.rb | 173 ++++++++++++++++-- lib/solargraph/gem_pins.rb | 3 +- lib/solargraph/parser/node_methods.rb | 2 +- .../parser/parser_gem/node_methods.rb | 2 +- lib/solargraph/pin/parameter.rb | 12 +- lib/solargraph/range.rb | 3 +- lib/solargraph/source.rb | 2 +- lib/solargraph/type_checker.rb | 40 +++- lib/solargraph/type_checker/checks.rb | 124 ------------- lib/solargraph/type_checker/rules.rb | 6 +- spec/complex_type_spec.rb | 28 +++ spec/type_checker/levels/strong_spec.rb | 16 ++ spec/type_checker/levels/typed_spec.rb | 49 +++++ 15 files changed, 373 insertions(+), 153 deletions(-) delete mode 100644 lib/solargraph/type_checker/checks.rb diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 9e23eb502..53c28ed6e 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -102,7 +102,7 @@ def each_unique_type &block # @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) } + atype.conforms_to?(api_map, self, :assignment) end # @return [Integer] @@ -176,6 +176,61 @@ def desc rooted_tags end + # @param api_map [ApiMap] + # @param expected [ComplexType, ComplexType::UniqueType] + # @param situation [:method_call, :return_type, :assignment] + # @param allow_subtype_skew [Boolean] if false, check if any + # subtypes of the expected type match the inferred type + # @param allow_reverse_match [Boolean] if true, check if any subtypes + # of the expected type match the inferred type + # @param allow_empty_params [Boolean] if true, allow a general + # inferred type without parameters to allow a more specific + # expcted type + # @param allow_any_match [Boolean] if true, any unique type + # matched in the expected qualifies as a match + # @return [Boolean] + def conforms_to? api_map, expected, + situation, + variance: erased_variance(situation), + allow_subtype_skew: false, + allow_empty_params: false, + allow_reverse_match: false, + allow_any_match: false #, +# allow_undefined_in_expected: false + expected = expected.downcast_to_literal_if_possible + inferred = downcast_to_literal_if_possible + + return duck_types_match?(api_map, expected, inferred) if expected.duck_type? + + if allow_any_match + inferred.any? { |inf| inf.conforms_to?(api_map, expected, situation, + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) } + else + inferred.all? { |inf| inf.conforms_to?(api_map, expected, situation, + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) } + end + end + + # @param api_map [ApiMap] + # @param expected [ComplexType] + # @param inferred [ComplexType] + # @return [Boolean] + def duck_types_match? api_map, expected, inferred + raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? + expected.each do |exp| + next unless exp.duck_type? + quack = exp.to_s[1..-1] + return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? + end + true + end + def rooted_tags map(&:rooted_tag).join(', ') end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index e6d596244..4fcaadb7f 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -69,6 +69,15 @@ def undefined? name == 'undefined' end + # Variance of the type ignoring any type parameters + def erased_variance situation = :method_call + if [:method_call, :return_type, :assignment].include?(situation) + :covariant + else + raise "Unknown situation: #{situation.inspect}" + end + end + # @param generics_to_erase [Enumerable] # @return [self] def erase_generics(generics_to_erase) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 0f4ec430d..1023d080e 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -151,10 +151,167 @@ def ==(other) eql?(other) end + # https://www.playfulpython.com/type-hinting-covariance-contra-variance/ + + # "[Expected] type variables that are COVARIANT can be substituted with + # a more specific [inferred] type without causing errors" + # + # "[Expected] type variables that are CONTRAVARIANT can be substituted + # with a more general [inferred] type without causing errors" + # + # "[Expected] types where neither is possible are INVARIANT" + # + # @param situation [:method_call] + # @param default [Symbol] The default variance to return if the type is not one of the special cases + # + # @return [:invariant, :covariant, :contravariant] + def parameter_variance situation, default = :covariant + # @todo RBS can specify variance - maybe we can use that info + # and also let folks specify? + # + # Array/Set: ideally invariant, since we don't know if user is + # going to add new stuff into it or read it. But we don't + # have a way to specify, so we use covariant + # Enumerable: covariant: can't be changed, so we can pass + # in more specific subtypes + # Hash: read-only would be covariant, read-write would be + # invariant if we could distinguish that - should default to + # covariant + # contravariant?: Proc - can be changed, so we can pass + # in less specific super types + if ['Hash', 'Tuple', 'Array', 'Set', 'Enumerable'].include?(name) && fixed_parameters? + :covariant + else + default + end + end + + # @param api_map [ApiMap] + # @param expected [ComplexType, ComplexType::UniqueType] + # @param situation [:method_call, :return_type] + # @param allow_subtype_skew [Boolean] if false, check if any + # subtypes of the expected type match the inferred type + # @param allow_empty_params [Boolean] if true, allow a general + # inferred type without parameters to allow a more specific + # expcted type + # @param allow_reverse_match [Boolean] if true, check if any subtypes + # of the expected type match the inferred type + # @param allow_any_match [Boolean] if true, any unique type + # matched in the expected qualifies as a match + def conforms_to_unique_type?(api_map, expected, situation = :method_call, + variance: erased_variance(situation), + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) + expected = expected.downcast_to_literal_if_possible + inferred = downcast_to_literal_if_possible + + if allow_subtype_skew + # parameters are not considered in this case + expected = expected.erase_parameters + end + + if !expected.parameters? && inferred.parameters? + inferred = inferred.erase_parameters + end + + return true if inferred == expected + + if variance == :invariant + return false unless inferred.name == expected.name + elsif erased_variance == :covariant + # covariant: we can pass in a more specific type + + # we contain the expected mix-in, or we have a more specific type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(expected.name, inferred.name) || + inferred.name == expected.name + + elsif erased_variance == :contravariant + # contravariant: we can pass in a more general type + + # we contain the expected mix-in, or we have a more general type + return false unless api_map.type_include?(inferred.name, expected.name) || + map.super_and_sub?(inferred.name, expected.name) || + inferred.name == expected.name + else + raise "Unknown erased variance: #{erased_variance.inspect}" + end + + return true if inferred.all_params.empty? && allow_empty_params + + # at this point we know the erased type is fine - time to look at parameters + + # there's an implicit 'any' on the expectation parameters + # if there are none specified + return true if expected.all_params.empty? + + unless expected.key_types.empty? + return false if inferred.key_types.empty? + + return false unless ComplexType.new(inferred.key_types).conforms_to?(api_map, + ComplexType.new(expected.key_types), + situation, + variance: parameter_variance(situation), + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) + end + + return true if expected.subtypes.empty? + + return false if inferred.subtypes.empty? + + ComplexType.new(inferred.subtypes).conforms_to?(api_map, ComplexType.new(expected.subtypes), situation, + variance: parameter_variance(situation), + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) + end + + # @param api_map [ApiMap] + # @param expected [ComplexType::UniqueType] + # @param situation [:method_call, :assignment, :return] + # @param allow_subtype_skew [Boolean] if false, check if any + # subtypes of the expected type match the inferred type + # @param allow_empty_params [Boolean] if true, allow a general + # inferred type without parameters to allow a more specific + # expcted type + # @param allow_reverse_match [Boolean] if true, check if any subtypes + # of the expected type match the inferred type + # @param allow_any_match [Boolean] if true, any unique type + # matched in the expected qualifies as a match + def conforms_to?(api_map, expected, + situation = :method_call, + allow_subtype_skew:, + allow_empty_params:, + allow_reverse_match:, + allow_any_match:) + # @todo teach this to validate duck types as inferred type + return true if duck_type? + + # complex types as expectations are unions - we only need to + # match one of their unique types + expected.any? do |expected_unique_type| + conforms_to_unique_type?(api_map, expected_unique_type, situation, + allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: allow_reverse_match, + allow_any_match: allow_any_match) + end + end + def hash [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash end + def erase_parameters + UniqueType.new(name, rooted: rooted?, parameters_type: parameters_type) + end + # @return [Array] def items [self] @@ -236,18 +393,6 @@ 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) @@ -437,6 +582,10 @@ def self_to_type dst end end + def any? &block + block.yield self + end + def all_rooted? return true if name == GENERIC_TAG_NAME rooted? && all_params.all?(&:rooted?) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index b92cbd6af..f1dd25a9f 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -19,7 +19,8 @@ def self.build_yard_pins(gemspec) YardMap::Mapper.new(yardoc, gemspec).map end - # @param pins [Array] + # @param pins [Array] + # @return [Array] def self.combine_method_pins_by_path(pins) # 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 } diff --git a/lib/solargraph/parser/node_methods.rb b/lib/solargraph/parser/node_methods.rb index 12e974c16..2712f2867 100644 --- a/lib/solargraph/parser/node_methods.rb +++ b/lib/solargraph/parser/node_methods.rb @@ -74,7 +74,7 @@ def process node # @abstract # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Source::Chain}] + # @return [Hash{Parser::AST::Node, Symbol => Source::Chain}] def convert_hash node raise NotImplementedError end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b716b352d..bc0c37eb6 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -120,7 +120,7 @@ def drill_signature node, signature end # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Chain}] + # @return [Hash{Parser::AST::Node, Symbol => Chain}] def convert_hash node return {} unless Parser.is_ast_node?(node) return convert_hash(node.children[0]) if node.type == :kwsplat diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index bc802b748..b4fc3d9b2 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -166,7 +166,17 @@ 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? + return true if ptype.undefined? + + return true if atype.conforms_to?(api_map, + ptype, + :method_call, + allow_subtype_skew: false, + allow_reverse_match: false, + allow_empty_params: true, + allow_any_match: false) + + ptype.generic? end def documentation diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 615f180af..c508e48fa 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -24,6 +24,7 @@ def initialize start, ending [start, ending] end + # @param other [Object] def <=>(other) return nil unless other.is_a?(Range) if start == other.start @@ -78,7 +79,7 @@ def self.from_to l1, c1, l2, c2 # Get a range from a node. # - # @param node [Parser::AST::Node] + # @param node [AST::Node] # @return [Range, nil] def self.from_node node if node&.loc && node.loc.expression diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index 11ab215ed..d4e0c3994 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -187,7 +187,7 @@ def code_for(node) frag.strip.gsub(/,$/, '') end - # @param node [Parser::AST::Node] + # @param node [AST::Node] # @return [String, nil] def comments_for node rng = Range.from_node(node) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index aa215f97b..ab87d5863 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -7,9 +7,7 @@ class TypeChecker autoload :Problem, 'solargraph/type_checker/problem' autoload :ParamDef, 'solargraph/type_checker/param_def' autoload :Rules, 'solargraph/type_checker/rules' - autoload :Checks, 'solargraph/type_checker/checks' - include Checks include Parser::NodeMethods # @return [String] @@ -113,7 +111,11 @@ def method_return_type_problems_for pin result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin) end else - unless (rules.require_all_return_types_match_inferred? ? all_types_match?(api_map, inferred, declared) : any_types_match?(api_map, declared, inferred)) + unless inferred.conforms_to?(api_map, declared, :return_type, + allow_subtype_skew: false, + allow_empty_params: !rules.require_inferred_type_params, + allow_reverse_match: false, + allow_any_match: !rules.require_all_unique_types_match_declared?) result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin) end end @@ -202,7 +204,11 @@ def variable_type_tag_problems result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin) end else - unless any_types_match?(api_map, declared, inferred) + unless inferred.conforms_to?(api_map, declared, :assignment, + allow_subtype_skew: false, + allow_empty_params: !rules.require_inferred_type_params, + allow_reverse_match: false, + allow_any_match: !rules.require_all_unique_types_match_declared?) result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin) end end @@ -284,8 +290,8 @@ def call_problems # @param chain [Solargraph::Source::Chain] # @param api_map [Solargraph::ApiMap] - # @param block_pin [Solargraph::Pin::Base] - # @param locals [Array] + # @param closure_pin [Solargraph::Pin::Closure] + # @param locals [Array] # @param location [Solargraph::Location] # @return [Array] def argument_problems_for chain, api_map, block_pin, locals, location @@ -383,7 +389,11 @@ def argument_problems_for chain, api_map, block_pin, locals, location # @todo Some level (strong, I guess) should require the param here else argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) + if argtype.defined? && ptype.defined? && !argtype.conforms_to?(api_map, ptype, :method_call, + allow_subtype_skew: false, + allow_empty_params: !rules.require_inferred_type_params, + allow_reverse_match: false, + allow_any_match: !rules.require_all_unique_types_match_declared?) errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") next end @@ -433,8 +443,13 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, else ptype = data[:qualified] unless ptype.undefined? + argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) + if argtype.defined? && ptype && !argtype.conforms_to?(api_map, ptype, :method_call, + allow_subtype_skew: false, + allow_empty_params: !rules.require_inferred_type_params, + allow_reverse_match: false, + allow_any_match: !rules.require_all_unique_types_match_declared?) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end end @@ -460,7 +475,12 @@ def kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kw next unless params.key?(pname.to_s) ptype = params[pname.to_s][:qualified] argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) + if argtype.defined? && ptype && !argtype.conforms_to?(api_map, ptype, :method_call, + allow_subtype_skew: false, + allow_empty_params: !rules.require_inferred_type_params, + allow_reverse_match: false, + allow_any_match: !rules.require_all_unique_types_match_declared?) + result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end end @@ -495,6 +515,8 @@ def param_hash(pin) end # @param pins [Array] + # @param method_pin_stack [Array] + # # @return [Hash{String => Hash{Symbol => String, ComplexType}}] def first_param_hash(pins) return {} if pins.empty? diff --git a/lib/solargraph/type_checker/checks.rb b/lib/solargraph/type_checker/checks.rb deleted file mode 100644 index de402978b..000000000 --- a/lib/solargraph/type_checker/checks.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Helper methods for performing type checks - # - module Checks - module_function - - # Compare an expected type with an inferred type. Common usage is to - # check if the type declared in a method's @return tag matches the type - # inferred from static analysis of the code. - # - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def types_match? api_map, expected, inferred - return true if expected.to_s == inferred.to_s - matches = [] - expected.each do |exp| - found = false - inferred.each do |inf| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - matches.push inf - break - end - end - return false unless found - end - inferred.each do |inf| - next if matches.include?(inf) - found = false - expected.each do |exp| - # if api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - if either_way?(api_map, inf, exp) - found = true - break - end - end - return false unless found - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @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 - expected.each do |exp| - next if exp.duck_type? - # @todo: there should be a level of typechecking where all - # unique types in the inferred must match one of the - # expected unique types - inferred.each do |inf| - # return true if exp == inf || api_map.super_and_sub?(fuzz(inf), fuzz(exp)) - return true if exp == inf || either_way?(api_map, inf, exp) - end - end - false - end - - # @param api_map [ApiMap] - # @param inferred [ComplexType] - # @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? - return false unless expected.any? { |exp| exp == inf || either_way?(api_map, inf, exp) } - end - true - end - - # @param api_map [ApiMap] - # @param expected [ComplexType] - # @param inferred [ComplexType] - # @return [Boolean] - def duck_types_match? api_map, expected, inferred - raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? - expected.each do |exp| - next unless exp.duck_type? - quack = exp.to_s[1..-1] - return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? - end - true - end - - # @param type [ComplexType::UniqueType] - # @return [String] - def fuzz type - if type.parameters? - type.name - else - type.tag - end - end - - # @param api_map [ApiMap] - # @param cls1 [ComplexType::UniqueType] - # @param cls2 [ComplexType::UniqueType] - # @return [Boolean] - def either_way?(api_map, cls1, cls2) - # @todo there should be a level of typechecking which uses the - # full tag with parameters to determine compatibility - f1 = cls1.name - f2 = cls2.name - api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - # api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1) - end - end - end -end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 0aad5ed8a..8f2027d30 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -54,7 +54,11 @@ def validate_tags? rank > LEVELS[:normal] end - def require_all_return_types_match_inferred? + def require_inferred_type_params + rank >= LEVELS[:alpha] + end + + def require_all_unique_types_match_declared? rank >= LEVELS[:alpha] end end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index f876d642f..2c060ceed 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -733,5 +733,33 @@ def make_bar expect(type.to_rbs).to eq('[Symbol, String, [Integer, Integer]]') expect(type.to_s).to eq('Array(Symbol, String, Array(Integer, Integer))') end + + it 'recognizes String conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('String') + atype = Solargraph::ComplexType.parse('String') + expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + end + + it 'recognizes an erased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Hash') + atype = Solargraph::ComplexType.parse('Hash') + expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + end + + it 'recognizes an unerased container type conforms with itself' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Array') + atype = Solargraph::ComplexType.parse('Array') + expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + end + + it 'recognizes a literal conforms with its type' do + api_map = Solargraph::ApiMap.new + ptype = Solargraph::ComplexType.parse('Symbol') + atype = Solargraph::ComplexType.parse(':foo') + expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + end end end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 12db1e442..054a09efa 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -14,6 +14,22 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end + + it 'ignores nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'reports missing param tags' do checker = type_checker(%( class Foo diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index b10bbd42c..659ccee39 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -38,6 +38,19 @@ def bar expect(checker.problems.first.message).to include('does not match') end + it 'reports mismatched key and subtypes ' do + checker = type_checker(%( + # @return [Hash{String => String}] + def foo + # @type h [Hash{Integer => String}] + h = {} + h + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match') + end + it 'reports mismatched inherited return tags' do checker = type_checker(%( class Sup @@ -189,6 +202,42 @@ def foo expect(checker.problems).to be_empty end + it 'validates parameters in function calls' do + checker = type_checker(%( + # @param bar [String] + def foo(bar); end + + def baz + foo(123) + end + )) + expect(checker.problems.map(&:message)).to eq(['123']) + end + + it 'validates default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 123); end + )) + expect(checker.problems.map(&:message)).to eq(['Declared type String does not match inferred type 123 for variable bar']) + end + + it 'validates string default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 'foo'); end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'validates symbol default values of parameters' do + checker = type_checker(%( + # @param bar [Symbol] + def foo(bar = :baz); end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'validates subclass arguments of param types' do checker = type_checker(%( class Sup From 8e9cb6f19710bb9bdaefa09dbcb350b4a90685e8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 18 Jul 2025 18:37:35 -0400 Subject: [PATCH 017/524] Add alpha typechecking spec --- spec/type_checker/levels/alpha_spec.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 spec/type_checker/levels/alpha_spec.rb diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb new file mode 100644 index 000000000..d700ea3b7 --- /dev/null +++ b/spec/type_checker/levels/alpha_spec.rb @@ -0,0 +1,22 @@ +describe Solargraph::TypeChecker do + context 'alpha level' do + def type_checker(code) + Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + end + + it 'reports nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq(["Wrong argument type for #foo: a expected String, received String, nil"]) + end + end +end From b9afcf7197159b5bcb30b0bd83cab8bebf7f4138 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 20 Jul 2025 17:21:56 -0400 Subject: [PATCH 018/524] Fix function defaults --- lib/solargraph/complex_type/unique_type.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 1023d080e..972bb0dbb 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -200,10 +200,10 @@ def parameter_variance situation, default = :covariant # matched in the expected qualifies as a match def conforms_to_unique_type?(api_map, expected, situation = :method_call, variance: erased_variance(situation), - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) + allow_subtype_skew:, + allow_empty_params:, + allow_reverse_match:, + allow_any_match:) expected = expected.downcast_to_literal_if_possible inferred = downcast_to_literal_if_possible From c2f4d73dc435b7fe591f8c92a5a40dc2866b821a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 20 Jul 2025 17:34:59 -0400 Subject: [PATCH 019/524] Adapt specs --- lib/solargraph/complex_type/unique_type.rb | 7 ++++ .../conforms_to_spec.rb} | 42 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) rename spec/{type_checker/checks_spec.rb => complex_type/conforms_to_spec.rb} (75%) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 972bb0dbb..6bbe9a1bd 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -204,6 +204,13 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, allow_empty_params:, allow_reverse_match:, allow_any_match:) + if allow_reverse_match + reversed_match = expected.conforms_to_unique_type? api_map, self, situation, allow_subtype_skew: allow_subtype_skew, + allow_empty_params: allow_empty_params, + allow_reverse_match: false, + allow_any_match: allow_any_match + return true if reversed_match + end expected = expected.downcast_to_literal_if_possible inferred = downcast_to_literal_if_possible diff --git a/spec/type_checker/checks_spec.rb b/spec/complex_type/conforms_to_spec.rb similarity index 75% rename from spec/type_checker/checks_spec.rb rename to spec/complex_type/conforms_to_spec.rb index 41119cefd..847da8563 100644 --- a/spec/type_checker/checks_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -1,9 +1,9 @@ -describe Solargraph::TypeChecker::Checks do +describe Solargraph::ComplexType do it 'validates simple core types' do api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('String') inf = Solargraph::ComplexType.parse('String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -11,7 +11,7 @@ api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('String') inf = Solargraph::ComplexType.parse('Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(false) end @@ -24,7 +24,7 @@ class Sub < Sup; end api_map.map source sup = Solargraph::ComplexType.parse('Sup') sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, sup, sub) + match = sub.conforms_to?(api_map, sup, :method_call) expect(match).to be(true) end @@ -48,7 +48,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Array') inf = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -59,7 +59,7 @@ class Sub < Sup; end api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) exp = Solargraph::ComplexType.parse('Set') inf = Solargraph::ComplexType.parse('Set') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -67,7 +67,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Hash{ Symbol => String}') inf = Solargraph::ComplexType.parse('Hash') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call, allow_empty_params: true) expect(match).to be(true) end @@ -75,7 +75,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('String, Integer') inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -83,7 +83,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('String, Integer') inf = Solargraph::ComplexType.parse('Integer, String') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -91,7 +91,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('String') inf = Solargraph::ComplexType.parse('String, Integer') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(false) end @@ -99,7 +99,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('nil') inf = Solargraph::ComplexType.parse('nil') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -107,7 +107,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Class') inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -115,7 +115,15 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Class') inf = Solargraph::ComplexType.parse('Class') - match = Solargraph::TypeChecker::Checks.types_match?(api_map, exp, inf) + match = inf.conforms_to?(api_map, exp, :method_call, allow_empty_params: true) + expect(match).to be(true) + end + + it 'validates generic classes with expected Class' do + api_map = Solargraph::ApiMap.new + inf = Solargraph::ComplexType.parse('Class') + exp = Solargraph::ComplexType.parse('Class') + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -128,9 +136,9 @@ class Sub < Sup; end api_map.map source sup = Solargraph::ComplexType.parse('Sup') sub = Solargraph::ComplexType.parse('Sub') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) + match = sub.conforms_to?(api_map, sup, :method_call, allow_reverse_match: true) expect(match).to be(true) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) + match = sup.conforms_to?(api_map, sub, :method_call, allow_reverse_match: true) expect(match).to be(true) end @@ -138,9 +146,9 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new sup = Solargraph::ComplexType.parse('String') sub = Solargraph::ComplexType.parse('Array') - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sup, sub) + match = sub.conforms_to?(api_map, sup, :method_call, allow_reverse_match: true) expect(match).to be(false) - match = Solargraph::TypeChecker::Checks.either_way?(api_map, sub, sup) + match = sup.conforms_to?(api_map, sub, :method_call, allow_reverse_match: true) expect(match).to be(false) end end From e0cead948bde8952ebd9241dfd41ff96a0a92887 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 20 Jul 2025 18:41:10 -0400 Subject: [PATCH 020/524] Fix some annotations --- lib/solargraph/api_map/store.rb | 2 +- lib/solargraph/pin/method.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 47f92194c..15fb00827 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -116,7 +116,7 @@ def get_instance_variables(fqns, scope = :instance) end # @param fqns [String] - # @return [Enumerable] + # @return [Enumerable] def get_class_variables(fqns) namespace_children(fqns).select { |pin| pin.is_a?(Pin::ClassVariable)} end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 2f807f444..749868246 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -385,7 +385,7 @@ def probe api_map attribute? ? infer_from_iv(api_map) : infer_from_return_nodes(api_map) end - # @return [::Array] + # @return [::Array] def overloads # Ignore overload tags with nil parameters. If it's not an array, the # tag's source is likely malformed. From e6c5a589fc1351baae3e45d8c107c93450da50fd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 22 Jul 2025 08:43:45 -0400 Subject: [PATCH 021/524] Generic typechecking improvements --- lib/solargraph/api_map/index.rb | 19 ++- lib/solargraph/complex_type.rb | 57 ++++--- lib/solargraph/complex_type/unique_type.rb | 120 +++++++------ .../data_definition/data_assignment_node.rb | 1 + .../data_definition/data_definition_node.rb | 3 +- .../struct_assignment_node.rb | 2 + .../struct_definition_node.rb | 3 +- lib/solargraph/gem_pins.rb | 5 +- lib/solargraph/language_server/host.rb | 3 +- .../parser/flow_sensitive_typing.rb | 6 + lib/solargraph/parser/node_processor.rb | 5 +- .../parser/parser_gem/node_methods.rb | 2 +- .../parser_gem/node_processors/if_node.rb | 2 + lib/solargraph/parser/snippet.rb | 2 +- lib/solargraph/pin/base.rb | 8 +- lib/solargraph/pin/method.rb | 2 +- lib/solargraph/pin/parameter.rb | 6 +- lib/solargraph/rbs_map/conversions.rb | 6 +- lib/solargraph/source/chain/if.rb | 2 +- lib/solargraph/source/chain/or.rb | 2 +- lib/solargraph/type_checker.rb | 61 ++++--- lib/solargraph/type_checker/rules.rb | 26 ++- lib/solargraph/yard_map/mapper/to_method.rb | 4 +- spec/complex_type/conforms_to_spec.rb | 12 +- spec/complex_type_spec.rb | 8 +- spec/rbs_map/conversions_spec.rb | 87 ++++++---- spec/type_checker/levels/strong_spec.rb | 161 ++++++++++++++++++ 27 files changed, 435 insertions(+), 180 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 810600534..42bb6cc32 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -34,7 +34,7 @@ def path_pin_hash # @param klass [Class>] # @return [Set>] def pins_by_class klass - # @type [Set] + # @type [Set>] s = Set.new @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end @@ -59,7 +59,8 @@ def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end - # @param pins [Array] + # @param pins [Enumerable] + # @return [self] def merge pins deep_clone.catalog pins end @@ -69,8 +70,9 @@ def merge pins attr_writer :pins, :pin_select_cache, :namespace_hash, :pin_class_hash, :path_pin_hash, :include_references, :extend_references, :prepend_references, :superclass_references + # @return [Solargraph::ApiMap::Index] def deep_clone - Index.allocate.tap do |copy| + out = Index.allocate.tap do |copy| copy.pin_select_cache = {} copy.pins = pins.clone %i[ @@ -81,9 +83,11 @@ def deep_clone copy.send(sym)&.transform_values!(&:clone) end end + out end - # @param new_pins [Array] + # @param new_pins [Enumerable] + # @return [self] def catalog new_pins @pin_select_cache = {} pins.concat new_pins @@ -104,7 +108,7 @@ def catalog new_pins end # @param klass [Class] - # @param hash [Hash{String => Array}] + # @param hash [Hash{String => Array}] # @return [void] def map_references klass, hash pins_by_class(klass).each do |pin| @@ -114,7 +118,7 @@ def map_references klass, hash # Add references to a map # - # @param hash [Hash{String => Array}] + # @param hash [Hash{String => Array}] # @param reference_pin [Pin::Reference] # # @return [void] @@ -138,9 +142,12 @@ def map_overrides pins = path_pin_hash[ovr.name] logger.debug { "ApiMap::Index#map_overrides: pins for path=#{ovr.name}: #{pins}" } pins.each do |pin| + next unless pin.is_a?(Pin::Reference::Override) + new_pin = if pin.path.end_with?('#initialize') path_pin_hash[pin.path.sub(/#initialize/, '.new')].first end + next unless new_pin.nil? || new_pin.is_a?(Pin::Method) (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 diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 53c28ed6e..05e499998 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -17,15 +17,15 @@ def initialize types = [UniqueType::UNDEFINED] # @todo @items here should not need an annotation # @type [Array] items = types.flat_map(&:items).uniq(&:to_s) + if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' } items.delete_if { |i| i.name == 'false' || i.name == 'true' } - items.unshift(ComplexType::BOOLEAN) + items.unshift(UniqueType::BOOLEAN) end items = [UniqueType::UNDEFINED] if items.any?(&:undefined?) @items = items end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [self.class, items] end @@ -76,9 +76,13 @@ def self_to_type dst end # @yieldparam [UniqueType] + # @yieldreturn [UniqueType] # @return [Array] - def map &block - @items.map &block + # @sg-ignore Declared return type + # ::Array<::Solargraph::ComplexType::UniqueType> does not match + # inferred type ::Array<::Proc> for Solargraph::ComplexType#map + def map(&block) + @items.map(&block) end # @yieldparam [UniqueType] @@ -155,10 +159,12 @@ def to_s map(&:tag).join(', ') end + # @return [String] def tags map(&:tag).join(', ') end + # @return [String] def simple_tags simplify_literals.tags end @@ -172,6 +178,7 @@ def downcast_to_literal_if_possible ComplexType.new(items.map(&:downcast_to_literal_if_possible)) end + # @return [String] def desc rooted_tags end @@ -184,36 +191,34 @@ def desc # @param allow_reverse_match [Boolean] if true, check if any subtypes # of the expected type match the inferred type # @param allow_empty_params [Boolean] if true, allow a general - # inferred type without parameters to allow a more specific - # expcted type + # inferred type without parameters to conform to a more specific + # expected type # @param allow_any_match [Boolean] if true, any unique type - # matched in the expected qualifies as a match + # matched in the inferred qualifies as a match + # @param allow_undefined [Boolean] if true, treat undefined as a + # wildcard that matches anything + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] # @return [Boolean] - def conforms_to? api_map, expected, + def conforms_to?(api_map, expected, situation, - variance: erased_variance(situation), - allow_subtype_skew: false, - allow_empty_params: false, - allow_reverse_match: false, - allow_any_match: false #, -# allow_undefined_in_expected: false + rules = [], + variance: erased_variance(situation)) expected = expected.downcast_to_literal_if_possible inferred = downcast_to_literal_if_possible return duck_types_match?(api_map, expected, inferred) if expected.duck_type? - if allow_any_match - inferred.any? { |inf| inf.conforms_to?(api_map, expected, situation, - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) } + if rules.include? :allow_any_match + inferred.any? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end else - inferred.all? { |inf| inf.conforms_to?(api_map, expected, situation, - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) } + inferred.all? do |inf| + inf.conforms_to?(api_map, expected, situation, rules, + variance: variance) + end end end @@ -231,6 +236,7 @@ def duck_types_match? api_map, expected, inferred true end + # @return [String] def rooted_tags map(&:rooted_tag).join(', ') end @@ -255,6 +261,7 @@ def generic? any?(&:generic?) end + # @return [ComplexType] def simplify_literals ComplexType.new(map(&:simplify_literals)) end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 6bbe9a1bd..361fe06bb 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -11,7 +11,6 @@ class UniqueType attr_reader :all_params, :subtypes, :key_types - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [@name, @all_params, @subtypes, @key_types] end @@ -78,6 +77,7 @@ def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: ni if parameters_type.nil? raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty? end + raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::') @name = name @parameters_type = parameters_type @@ -105,6 +105,7 @@ def to_s tag end + # @return [self] def simplify_literals transform do |t| next t unless t.literal? @@ -116,10 +117,12 @@ def literal? non_literal_name != name end + # @return [String] def non_literal_name @non_literal_name ||= determine_non_literal_name end + # @return [String] def determine_non_literal_name # https://github.com/ruby/rbs/blob/master/docs/syntax.md # @@ -186,35 +189,37 @@ def parameter_variance situation, default = :covariant end end + # Whether this is an RBS interface like _ToAry or _Each. + def interface? + name.start_with?('_') + end + # @param api_map [ApiMap] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] # @param situation [:method_call, :return_type] - # @param allow_subtype_skew [Boolean] if false, check if any - # subtypes of the expected type match the inferred type - # @param allow_empty_params [Boolean] if true, allow a general - # inferred type without parameters to allow a more specific - # expcted type - # @param allow_reverse_match [Boolean] if true, check if any subtypes - # of the expected type match the inferred type - # @param allow_any_match [Boolean] if true, any unique type - # matched in the expected qualifies as a match + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] def conforms_to_unique_type?(api_map, expected, situation = :method_call, - variance: erased_variance(situation), - allow_subtype_skew:, - allow_empty_params:, - allow_reverse_match:, - allow_any_match:) - if allow_reverse_match - reversed_match = expected.conforms_to_unique_type? api_map, self, situation, allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: false, - allow_any_match: allow_any_match + rules = [], + variance: erased_variance(situation)) + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" unless expected.is_a?(UniqueType) + if literal? && !expected.literal? + return simplify_literals.conforms_to_unique_type?(api_map, expected, situation, + rules, variance: variance) + end + return true if expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface) + return true if interface? && rules.include?(:allow_unmatched_interface) + + if rules.include? :allow_reverse_match + reversed_match = expected.conforms_to?(api_map, self, situation, + rules - [:allow_reverse_match], + variance: variance) return true if reversed_match end expected = expected.downcast_to_literal_if_possible inferred = downcast_to_literal_if_possible - if allow_subtype_skew + if rules.include? :allow_subtype_skew # parameters are not considered in this case expected = expected.erase_parameters end @@ -223,6 +228,10 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, inferred = inferred.erase_parameters end + if expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) + expected = expected.erase_parameters + end + return true if inferred == expected if variance == :invariant @@ -240,13 +249,13 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, # we contain the expected mix-in, or we have a more general type return false unless api_map.type_include?(inferred.name, expected.name) || - map.super_and_sub?(inferred.name, expected.name) || + api_map.super_and_sub?(inferred.name, expected.name) || inferred.name == expected.name else raise "Unknown erased variance: #{erased_variance.inspect}" end - return true if inferred.all_params.empty? && allow_empty_params + return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) # at this point we know the erased type is fine - time to look at parameters @@ -260,54 +269,50 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, return false unless ComplexType.new(inferred.key_types).conforms_to?(api_map, ComplexType.new(expected.key_types), situation, - variance: parameter_variance(situation), - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) + rules, + variance: parameter_variance(situation)) end return true if expected.subtypes.empty? + return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + return false if inferred.subtypes.empty? - ComplexType.new(inferred.subtypes).conforms_to?(api_map, ComplexType.new(expected.subtypes), situation, - variance: parameter_variance(situation), - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) + ComplexType.new(inferred.subtypes).conforms_to?(api_map, + ComplexType.new(expected.subtypes), + situation, + rules, + variance: parameter_variance(situation)) end # @param api_map [ApiMap] - # @param expected [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType, ComplexType] # @param situation [:method_call, :assignment, :return] - # @param allow_subtype_skew [Boolean] if false, check if any - # subtypes of the expected type match the inferred type - # @param allow_empty_params [Boolean] if true, allow a general - # inferred type without parameters to allow a more specific - # expcted type - # @param allow_reverse_match [Boolean] if true, check if any subtypes - # of the expected type match the inferred type - # @param allow_any_match [Boolean] if true, any unique type - # matched in the expected qualifies as a match + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] + # @param variance [:invariant, :covariant, :contravariant] def conforms_to?(api_map, expected, situation = :method_call, - allow_subtype_skew:, - allow_empty_params:, - allow_reverse_match:, - allow_any_match:) + rules, + variance:) + + return true if undefined? && rules.include?(:allow_undefined) + # @todo teach this to validate duck types as inferred type return true if duck_type? # complex types as expectations are unions - we only need to # match one of their unique types expected.any? do |expected_unique_type| + raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" unless expected.is_a?(UniqueType) unless expected_unique_type.instance_of?(UniqueType) conforms_to_unique_type?(api_map, expected_unique_type, situation, - allow_subtype_skew: allow_subtype_skew, - allow_empty_params: allow_empty_params, - allow_reverse_match: allow_reverse_match, - allow_any_match: allow_any_match) + rules, variance: variance) end end @@ -315,6 +320,7 @@ def hash [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash end + # @return [self] def erase_parameters UniqueType.new(name, rooted: rooted?, parameters_type: parameters_type) end @@ -335,6 +341,7 @@ def rbs_name end end + # @return [String] def desc rooted_tags end @@ -407,7 +414,7 @@ def downcast_to_literal_if_possible # @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 + # @param resolved_generic_values [Hash{String => ComplexType, UniqueType}] Added to as types are encountered or resolved # @return [UniqueType, ComplexType] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} if name == ComplexType::GENERIC_TAG_NAME @@ -505,9 +512,9 @@ def to_a # @param new_name [String, nil] # @param make_rooted [Boolean, nil] - # @param new_key_types [Array, nil] + # @param new_key_types [Array, nil] # @param rooted [Boolean, nil] - # @param new_subtypes [Array, nil] + # @param new_subtypes [Array, nil] # @return [self] def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil) raise "Please remove leading :: and set rooted instead - #{new_name}" if new_name&.start_with?('::') @@ -589,6 +596,7 @@ def self_to_type dst end end + # @yieldreturn [Boolean] def any? &block block.yield self end diff --git a/lib/solargraph/convention/data_definition/data_assignment_node.rb b/lib/solargraph/convention/data_definition/data_assignment_node.rb index 7aadcf190..0ecfb88eb 100644 --- a/lib/solargraph/convention/data_definition/data_assignment_node.rb +++ b/lib/solargraph/convention/data_definition/data_assignment_node.rb @@ -22,6 +22,7 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar)))) + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :casgn return false if node.children[2].nil? diff --git a/lib/solargraph/convention/data_definition/data_definition_node.rb b/lib/solargraph/convention/data_definition/data_definition_node.rb index dd5929822..5ee79b73d 100644 --- a/lib/solargraph/convention/data_definition/data_definition_node.rb +++ b/lib/solargraph/convention/data_definition/data_definition_node.rb @@ -25,6 +25,7 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar))) + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :class @@ -46,7 +47,7 @@ def data_definition_node?(data_node) end end - # @return [Parser::AST::Node] + # @param node [Parser::AST::Node] def initialize(node) @node = node end diff --git a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb index 04f96d40e..2816de6ed 100644 --- a/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_assignment_node.rb @@ -22,6 +22,8 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar)))) + # + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :casgn return false if node.children[2].nil? diff --git a/lib/solargraph/convention/struct_definition/struct_definition_node.rb b/lib/solargraph/convention/struct_definition/struct_definition_node.rb index 540320c37..7c3d722d0 100644 --- a/lib/solargraph/convention/struct_definition/struct_definition_node.rb +++ b/lib/solargraph/convention/struct_definition/struct_definition_node.rb @@ -25,6 +25,7 @@ class << self # s(:def, :foo, # s(:args), # s(:send, nil, :bar))) + # @param node [Parser::AST::Node] def match?(node) return false unless node&.type == :class @@ -46,7 +47,7 @@ def struct_definition_node?(struct_node) end end - # @return [Parser::AST::Node] + # @param node [Parser::AST::Node] def initialize(node) @node = node end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index f1dd25a9f..2a3f392f6 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -25,13 +25,14 @@ def self.combine_method_pins_by_path(pins) # 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| + combined_by_path = by_path.transform_values do |pins| GemPins.combine_method_pins(*pins) end - by_path.values + alias_pins + combined_by_path.values + alias_pins end def self.combine_method_pins(*pins) + # @type [Pin::Method, nil] out = pins.reduce(nil) do |memo, pin| next pin if memo.nil? if memo == pin && memo.source != :combined diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index 1c5831bda..e85fc813a 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -299,6 +299,7 @@ def prepare directory, name = nil end end + # @return [String] def command_path options['commandPath'] || 'solargraph' end @@ -716,7 +717,7 @@ def diagnoser # A hash of client requests by ID. The host uses this to keep track of # pending responses. # - # @return [Hash{Integer => Solargraph::LanguageServer::Host}] + # @return [Hash{Integer => Request}] def requests @requests ||= {} end diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 8fb26d498..8dd80a5a0 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -11,7 +11,9 @@ def initialize(locals, enclosing_breakable_pin = nil) # @param and_node [Parser::AST::Node] def process_and(and_node, true_ranges = []) + # @type [Parser::AST::Node] lhs = and_node.children[0] + # @type [Parser::AST::Node] rhs = and_node.children[1] before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) @@ -36,7 +38,9 @@ def process_if(if_node) # s(:send, nil, :bar)) # [4] pry(main)> conditional_node = if_node.children[0] + # @type [Parser::AST::Node] then_clause = if_node.children[1] + # @type [Parser::AST::Node] else_clause = if_node.children[2] true_ranges = [] @@ -142,6 +146,8 @@ def process_facts(facts_by_pin, presences) end # @param conditional_node [Parser::AST::Node] + # @param true_ranges [Array] + # @return [void] def process_conditional(conditional_node, true_ranges) if conditional_node.type == :send process_isa(conditional_node, true_ranges) diff --git a/lib/solargraph/parser/node_processor.rb b/lib/solargraph/parser/node_processor.rb index a55b7120b..a1a4d811f 100644 --- a/lib/solargraph/parser/node_processor.rb +++ b/lib/solargraph/parser/node_processor.rb @@ -23,6 +23,9 @@ def register type, cls @@processors[type] << cls end + # @param type [Symbol] + # @param cls [Class] + # @return [void] def deregister type, cls @@processors[type].delete(cls) end @@ -31,7 +34,7 @@ def deregister type, cls # @param node [Parser::AST::Node] # @param region [Region] # @param pins [Array] - # @param locals [Array] + # @param locals [Array] # @return [Array(Array, Array)] def self.process node, region = Region.new, pins = [], locals = [] if pins.empty? diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index bc0c37eb6..674013257 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -345,7 +345,7 @@ def value_position_nodes_only(node) # Look at known control statements and use them to find # more specific return nodes. # - # @param node [Parser::AST::Node] Statement which is in + # @param node [AST::Node] Statement which is in # value position for a method body # @param include_explicit_returns [Boolean] If true, # include the value nodes of the parameter of the diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index 5784afcbe..2452b9cc5 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -11,6 +11,8 @@ def process process_children position = get_node_start_position(node) + # @sg-ignore + # @type [Solargraph::Pin::Breakable, nil] enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) end diff --git a/lib/solargraph/parser/snippet.rb b/lib/solargraph/parser/snippet.rb index d28c57c8c..3b609d31b 100644 --- a/lib/solargraph/parser/snippet.rb +++ b/lib/solargraph/parser/snippet.rb @@ -1,7 +1,7 @@ module Solargraph module Parser class Snippet - # @return [Range] + # @return [Solargraph::Range] attr_reader :range # @return [String] attr_reader :text diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index cdd6a5ace..c9e308056 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -269,9 +269,13 @@ def assert_same_count(other, attr) # @param other [self] # @param attr [::Symbol] # - # @return [Object, nil] + # @return [undefined] def assert_same(other, attr) - return false if other.nil? + if other.nil? + 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}") + return send(attr) + end val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 749868246..c3e29b8e3 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -50,7 +50,7 @@ def combine_all_signature_pins(*signature_pins) end # @param other [Pin::Method] - # @return [Symbol] + # @return [::Symbol] def combine_visibility(other) if dodgy_visibility_source? && !other.dodgy_visibility_source? other.visibility diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index b4fc3d9b2..d806d6de1 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -171,11 +171,7 @@ def compatible_arg?(atype, api_map) return true if atype.conforms_to?(api_map, ptype, :method_call, - allow_subtype_skew: false, - allow_reverse_match: false, - allow_empty_params: true, - allow_any_match: false) - + [:allow_empty_params, :allow_undefined]) ptype.generic? end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 6e50c022a..44fb72946 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -95,7 +95,7 @@ def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) generic_values = type.all_params.map(&:to_s) include_pin = Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -184,7 +184,7 @@ def class_decl_to_pin decl type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, generic_values: generic_values, - name: decl.super_class.name.relative!.to_s, + name: type.rooted_name, source: :rbs ) end @@ -229,6 +229,8 @@ def module_decl_to_pin decl convert_self_types_to_pins decl, module_pin convert_members_to_pins decl, module_pin + raise "Invalid type for module declaration: #{module_pin.class}" unless module_pin.is_a?(Pin::Namespace) + add_mixins decl, module_pin.closure end diff --git a/lib/solargraph/source/chain/if.rb b/lib/solargraph/source/chain/if.rb index c14d00ddf..3a7fa0ca9 100644 --- a/lib/solargraph/source/chain/if.rb +++ b/lib/solargraph/source/chain/if.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 1e3a70f40..9264d4107 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -8,7 +8,7 @@ def word '' end - # @param links [::Array] + # @param links [::Array] def initialize links @links = links end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index ab87d5863..953832a36 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -36,6 +36,35 @@ def source_map @source_map ||= api_map.source_map(filename) end + # @return [Source] + def source + @source_map.source + end + + def return_type_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :return_type) + end + + def arg_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :method_call) + end + + def assignment_conforms_to?(inferred, expected) + conforms_to?(inferred, expected, :assignment) + end + + def conforms_to?(inferred, expected, scenario) + rules_arr = [] + rules_arr << :allow_empty_params unless rules.require_inferred_type_params? + rules_arr << :allow_any_match unless rules.require_all_unique_types_match_declared? + rules_arr << :allow_undefined unless rules.require_no_undefined_args? + rules_arr << :allow_unresolved_generic unless rules.require_generics_resolved? + rules_arr << :allow_unmatched_interface unless rules.require_interfaces_resolved? + rules_arr << :allow_reverse_match unless rules.require_downcasts? + inferred.conforms_to?(api_map, expected, scenario, + rules_arr) + end + # @return [Array] def problems @problems ||= begin @@ -111,11 +140,7 @@ def method_return_type_problems_for pin result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin) end else - unless inferred.conforms_to?(api_map, declared, :return_type, - allow_subtype_skew: false, - allow_empty_params: !rules.require_inferred_type_params, - allow_reverse_match: false, - allow_any_match: !rules.require_all_unique_types_match_declared?) + unless return_type_conforms_to?(inferred, declared) result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin) end end @@ -204,11 +229,7 @@ def variable_type_tag_problems result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin) end else - unless inferred.conforms_to?(api_map, declared, :assignment, - allow_subtype_skew: false, - allow_empty_params: !rules.require_inferred_type_params, - allow_reverse_match: false, - allow_any_match: !rules.require_all_unique_types_match_declared?) + unless assignment_conforms_to?(inferred, declared) result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin) end end @@ -389,11 +410,7 @@ def argument_problems_for chain, api_map, block_pin, locals, location # @todo Some level (strong, I guess) should require the param here else argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype.defined? && !argtype.conforms_to?(api_map, ptype, :method_call, - allow_subtype_skew: false, - allow_empty_params: !rules.require_inferred_type_params, - allow_reverse_match: false, - allow_any_match: !rules.require_all_unique_types_match_declared?) + if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype) errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") next end @@ -443,13 +460,8 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, else ptype = data[:qualified] unless ptype.undefined? - argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !argtype.conforms_to?(api_map, ptype, :method_call, - allow_subtype_skew: false, - allow_empty_params: !rules.require_inferred_type_params, - allow_reverse_match: false, - allow_any_match: !rules.require_all_unique_types_match_declared?) + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end end @@ -475,12 +487,7 @@ def kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kw next unless params.key?(pname.to_s) ptype = params[pname.to_s][:qualified] argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype && !argtype.conforms_to?(api_map, ptype, :method_call, - allow_subtype_skew: false, - allow_empty_params: !rules.require_inferred_type_params, - allow_reverse_match: false, - allow_any_match: !rules.require_all_unique_types_match_declared?) - + if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 8f2027d30..33ec0c4d0 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -54,13 +54,37 @@ def validate_tags? rank > LEVELS[:normal] end - def require_inferred_type_params + def require_inferred_type_params? rank >= LEVELS[:alpha] end def require_all_unique_types_match_declared? rank >= LEVELS[:alpha] end + + def require_no_undefined_args? + rank >= LEVELS[:alpha] + end + + def require_generics_resolved? + rank >= LEVELS[:alpha] + end + + def require_interfaces_resolved? + rank >= LEVELS[:alpha] + end + + def require_downcasts? + rank >= LEVELS[:alpha] + end + + # We keep this at strong because if you added an @sg-ignore to + # address a strong-level issue, then ran at a lower level, you'd + # get a false positive - we don't run stronger level checks than + # requested for performance reasons + def validate_sg_ignores? + rank >= LEVELS[:strong] + end end end end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index df431bb3c..d8e3b8b43 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -27,8 +27,8 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = final_scope = scope || code_object.scope override_key = [closure.path, final_scope, name] final_visibility = VISIBILITY_OVERRIDE[override_key] - final_visibility ||= VISIBILITY_OVERRIDE[override_key[0..-2]] - final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name) + final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]] + final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym) final_visibility ||= visibility final_visibility ||= :private if code_object.module_function? && final_scope == :instance final_visibility ||= :public if code_object.module_function? && final_scope == :class diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index 847da8563..5755721b4 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -67,7 +67,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Hash{ Symbol => String}') inf = Solargraph::ComplexType.parse('Hash') - match = inf.conforms_to?(api_map, exp, :method_call, allow_empty_params: true) + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) expect(match).to be(true) end @@ -115,7 +115,7 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new exp = Solargraph::ComplexType.parse('Class') inf = Solargraph::ComplexType.parse('Class') - match = inf.conforms_to?(api_map, exp, :method_call, allow_empty_params: true) + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) expect(match).to be(true) end @@ -136,9 +136,9 @@ class Sub < Sup; end api_map.map source sup = Solargraph::ComplexType.parse('Sup') sub = Solargraph::ComplexType.parse('Sub') - match = sub.conforms_to?(api_map, sup, :method_call, allow_reverse_match: true) + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) expect(match).to be(true) - match = sup.conforms_to?(api_map, sub, :method_call, allow_reverse_match: true) + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) expect(match).to be(true) end @@ -146,9 +146,9 @@ class Sub < Sup; end api_map = Solargraph::ApiMap.new sup = Solargraph::ComplexType.parse('String') sub = Solargraph::ComplexType.parse('Array') - match = sub.conforms_to?(api_map, sup, :method_call, allow_reverse_match: true) + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) expect(match).to be(false) - match = sup.conforms_to?(api_map, sub, :method_call, allow_reverse_match: true) + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) expect(match).to be(false) end end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 2c060ceed..dd20099eb 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -738,28 +738,28 @@ def make_bar api_map = Solargraph::ApiMap.new ptype = Solargraph::ComplexType.parse('String') atype = Solargraph::ComplexType.parse('String') - expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end it 'recognizes an erased container type conforms with itself' do api_map = Solargraph::ApiMap.new ptype = Solargraph::ComplexType.parse('Hash') atype = Solargraph::ComplexType.parse('Hash') - expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end it 'recognizes an unerased container type conforms with itself' do api_map = Solargraph::ApiMap.new ptype = Solargraph::ComplexType.parse('Array') atype = Solargraph::ComplexType.parse('Array') - expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end it 'recognizes a literal conforms with its type' do api_map = Solargraph::ApiMap.new ptype = Solargraph::ComplexType.parse('Symbol') atype = Solargraph::ComplexType.parse(':foo') - expect(ptype.conforms_to?(api_map, atype, :method_call)).to be(true) + expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end end end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 09c203687..75ec1c311 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -1,54 +1,75 @@ 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 + context 'with custom RBS files' 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 - end - let(:rbs_repo) do - RBS::Repository.new(no_stdlib: false) - 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(:loader) do + RBS::EnvironmentLoader.new(core_root: nil, repository: rbs_repo) + end - let(:conversions) do - Solargraph::RbsMap::Conversions.new(loader: loader) - end + let(:conversions) do + Solargraph::RbsMap::Conversions.new(loader: loader) + end - let(:pins) do - conversions.pins - 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 + 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 + attr_reader :temp_dir - context 'with untyped response' do - let(:rbs) do - <<~RBS + 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 - subject(:method_pin) { pins.find { |pin| pin.path == 'Foo#bar' } } + context 'with standard loads for solargraph project' do + let(:api_map) { Solargraph::ApiMap.load('.') } - it { should_not be_nil } + let(:superclass_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' + end + end - it { should be_a(Solargraph::Pin::Method) } + it 'finds a superclass pin for Parser::AST::Node' do + expect(superclass_pin).not_to be_nil + end - it 'maps untyped in RBS to undefined in Solargraph 'do - expect(method_pin.return_type.tag).to eq('undefined') + it 'generates a rooted pin for superclass of Parser::AST::Node' do + # rooted! + expect(superclass_pin.name) .to eq('::AST::Node') end end end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 054a09efa..4c45056ea 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -141,6 +141,167 @@ def bar &block expect(checker.problems).to be_empty end + it 'does not need fully specified container types' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class>] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores undefined resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + + it 'ignores generic resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures' do + checker = type_checker(%( + # @generic T + # @param path [String] + # @param klass [Class>] + # @return [void] + def code_object_at path, klass = Integer + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on select { is_a? } pattern' do + checker = type_checker(%( + # @param arr [Enumerable} + # @return [Enumerable] + def downcast_arr(arr) + arr.select { |pin| pin.is_a?(Integer) } + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via return value' do + checker = type_checker(%( + # @param bar [Integer] + # @return [Integer, nil] + def foo(bar) + bar + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via select' do + checker = type_checker(%( + # @return [Float, nil]} + def bar; rand; end + + # @param arr [Enumerable} + # @return [Integer, nil] + def downcast_arr(arr) + # @type [Object, nil] + foo = arr.select { |pin| pin.is_a?(Integer) && bar }.last + foo + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'inherits param tags from superclass methods' do checker = type_checker(%( class Foo From 898bb87e30bdf9f9837bb986deb467e7b5324404 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 22 Jul 2025 09:11:41 -0400 Subject: [PATCH 022/524] Fix specs --- lib/solargraph/api_map/index.rb | 11 ++++------- lib/solargraph/doc_map.rb | 2 ++ lib/solargraph/rbs_map/core_map.rb | 5 ++++- spec/rbs_map/core_map_spec.rb | 2 +- spec/type_checker/levels/strict_spec.rb | 13 +++++++++++++ spec/type_checker/levels/typed_spec.rb | 12 ------------ 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 42bb6cc32..9015cd6d5 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -39,22 +39,22 @@ def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references @include_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references @extend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references @prepend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end @@ -142,12 +142,9 @@ def map_overrides pins = path_pin_hash[ovr.name] logger.debug { "ApiMap::Index#map_overrides: pins for path=#{ovr.name}: #{pins}" } pins.each do |pin| - next unless pin.is_a?(Pin::Reference::Override) - new_pin = if pin.path.end_with?('#initialize') path_pin_hash[pin.path.sub(/#initialize/, '.new')].first end - next unless new_pin.nil? || new_pin.is_a?(Pin::Method) (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 diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index d51fc3022..186037460 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -150,6 +150,8 @@ def load_serialized_gem_pins @uncached_yard_gemspecs = [] @uncached_rbs_collection_gemspecs = [] with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v } + # @sg-ignore Need Hash[] support + # @type [Array] paths = Hash[without_gemspecs].keys gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 0d265d773..5e030d9f6 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -24,8 +24,11 @@ def pins else loader.add(path: Pathname(FILLS_DIRECTORY)) @pins = conversions.pins + # add some overrides @pins.concat RbsMap::CoreFills::ALL - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + # process overrides, then remove any which couldn't be resolved + processed = ApiMap::Store.new(@pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + STDOUT.puts "RBS core pins cache size: #{@pins.size}" @pins.replace processed PinCache.serialize_core @pins diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 352d29937..88590925b 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -6,7 +6,7 @@ pin = store.get_path_pins("Errno::#{const}").first expect(pin).to be_a(Solargraph::Pin::Namespace) superclass = store.get_superclass(pin.path) - expect(superclass).to eq('SystemCallError') + expect(superclass).to eq('::SystemCallError') end end diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index b198cec89..7861c8817 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -666,6 +666,19 @@ def test(foo: nil) expect(checker.problems).to be_empty end + + it 'validates parameters in function calls' do + checker = type_checker(%( + # @param bar [String] + def foo(bar); end + + def baz + foo(123) + end + )) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type for #foo: bar expected String, received 123']) + end + it 'validates inferred return types with complex tags' do checker = type_checker(%( # @param foo [Numeric, nil] a foo diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index 659ccee39..6e71ee9ff 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -202,18 +202,6 @@ def foo expect(checker.problems).to be_empty end - it 'validates parameters in function calls' do - checker = type_checker(%( - # @param bar [String] - def foo(bar); end - - def baz - foo(123) - end - )) - expect(checker.problems.map(&:message)).to eq(['123']) - end - it 'validates default values of parameters' do checker = type_checker(%( # @param bar [String] From b6bfe7b4fd9c8a52038cc096bd29050e244b6fed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 22 Jul 2025 10:11:45 -0400 Subject: [PATCH 023/524] Generic typechecking improvements --- .../language_server/message/base.rb | 2 +- .../message/extended/check_gem_version.rb | 6 - rbs/fills/rubygems/0/spec_fetcher.rbs | 107 ++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 rbs/fills/rubygems/0/spec_fetcher.rbs diff --git a/lib/solargraph/language_server/message/base.rb b/lib/solargraph/language_server/message/base.rb index fbc55ccbd..b2df0c46a 100644 --- a/lib/solargraph/language_server/message/base.rb +++ b/lib/solargraph/language_server/message/base.rb @@ -16,7 +16,7 @@ class Base # @return [String] attr_reader :method - # @return [Hash{String => Array, Hash{String => undefined}, String, Integer}] + # @return [Hash{String => undefined}] attr_reader :params # @return [Hash, Array, nil] 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 2e80f40c6..06892ed19 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true -# @todo PR the RBS gem to add this -# @!parse -# module ::Gem -# class SpecFetcher; end -# end - module Solargraph module LanguageServer module Message diff --git a/rbs/fills/rubygems/0/spec_fetcher.rbs b/rbs/fills/rubygems/0/spec_fetcher.rbs new file mode 100644 index 000000000..9914dc85d --- /dev/null +++ b/rbs/fills/rubygems/0/spec_fetcher.rbs @@ -0,0 +1,107 @@ +# +# SpecFetcher handles metadata updates from remote gem repositories. +# +class Gem::SpecFetcher + self.@fetcher: untyped + + @sources: untyped + + @update_cache: untyped + + @specs: untyped + + @latest_specs: untyped + + @prerelease_specs: untyped + + @caches: untyped + + @fetcher: untyped + + include Gem::UserInteraction + + include Gem::Text + + attr_reader latest_specs: untyped + + attr_reader sources: untyped + + attr_reader specs: untyped + + attr_reader prerelease_specs: untyped + + # + # Default fetcher instance. Use this instead of ::new to reduce object + # allocation. + # + def self.fetcher: () -> untyped + + def self.fetcher=: (untyped fetcher) -> untyped + + # + # Creates a new SpecFetcher. Ordinarily you want to use the default fetcher + # from Gem::SpecFetcher::fetcher which uses the Gem.sources. + # + # If you need to retrieve specifications from a different `source`, you can send + # it as an argument. + # + def initialize: (?untyped? sources) -> void + + # + # Find and fetch gem name tuples that match `dependency`. + # + # If `matching_platform` is false, gems for all platforms are returned. + # + def search_for_dependency: (untyped dependency, ?bool matching_platform) -> ::Array[untyped] + + # + # Return all gem name tuples who's names match `obj` + # + def detect: (?::Symbol type) { (untyped) -> untyped } -> untyped + + # + # Find and fetch specs that match `dependency`. + # + # If `matching_platform` is false, gems for all platforms are returned. + # + def spec_for_dependency: (untyped dependency, ?bool matching_platform) -> ::Array[untyped] + + # + # Suggests gems based on the supplied `gem_name`. Returns an array of + # alternative gem names. + # + def suggest_gems_from_name: (untyped gem_name, ?::Symbol type, ?::Integer num_results) -> (::Array[untyped] | untyped) + + # + # Returns a list of gems available for each source in Gem::sources. + # + # `type` can be one of 3 values: :released => Return the list of all released + # specs :complete => Return the list of all specs :latest => Return the + # list of only the highest version of each gem :prerelease => Return the list of + # all prerelease only specs + # + def available_specs: (untyped type) -> ::Array[untyped] + + def tuples_for: (untyped source, untyped type, ?bool gracefully_ignore) -> untyped +end From 1d0b23d13f20646992a4b17a44c492826f433cab Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 22 Jul 2025 17:53:02 -0400 Subject: [PATCH 024/524] Generic typechecking improvements --- lib/solargraph/api_map/index.rb | 3 +-- lib/solargraph/complex_type/type_methods.rb | 2 ++ lib/solargraph/doc_map.rb | 1 + lib/solargraph/location.rb | 1 - lib/solargraph/position.rb | 1 - lib/solargraph/range.rb | 1 - lib/solargraph/type_checker.rb | 11 +++++++++++ lib/solargraph/type_checker/rules.rb | 2 +- 8 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 9015cd6d5..fbfb5c218 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -72,7 +72,7 @@ def merge pins # @return [Solargraph::ApiMap::Index] def deep_clone - out = Index.allocate.tap do |copy| + Index.allocate.tap do |copy| copy.pin_select_cache = {} copy.pins = pins.clone %i[ @@ -83,7 +83,6 @@ def deep_clone copy.send(sym)&.transform_values!(&:clone) end end - out end # @param new_pins [Enumerable] diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index 4fcaadb7f..791ab80b0 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -70,6 +70,8 @@ def undefined? end # Variance of the type ignoring any type parameters + # @return [Symbol] + # @param situation [Symbol] The situation in which the variance is being considered. def erased_variance situation = :method_call if [:method_call, :return_type, :assignment].include?(situation) :covariant diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 186037460..9136da26b 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -153,6 +153,7 @@ def load_serialized_gem_pins # @sg-ignore Need Hash[] support # @type [Array] paths = Hash[without_gemspecs].keys + # @type [Array] gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a paths.each do |path| diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 74d1318df..3af8016b3 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -20,7 +20,6 @@ def initialize filename, range @range = range end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [filename, range] end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 1bd31e0f5..27289d28f 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -21,7 +21,6 @@ def initialize line, character @character = character end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [line, character] end diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index c508e48fa..2bea62797 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -19,7 +19,6 @@ def initialize start, ending @ending = ending end - # @sg-ignore Fix "Not enough arguments to Module#protected" protected def equality_fields [start, ending] end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 953832a36..8b86f02df 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -41,18 +41,27 @@ def source @source_map.source end + # @param inferred [ComplexType] + # @param expected [ComplexType] def return_type_conforms_to?(inferred, expected) conforms_to?(inferred, expected, :return_type) end + # @param inferred [ComplexType] + # @param expected [ComplexType] def arg_conforms_to?(inferred, expected) conforms_to?(inferred, expected, :method_call) end + # @param inferred [ComplexType] + # @param expected [ComplexType] def assignment_conforms_to?(inferred, expected) conforms_to?(inferred, expected, :assignment) end + # @param inferred [ComplexType] + # @param expected [ComplexType] + # @param scenario [Symbol] def conforms_to?(inferred, expected, scenario) rules_arr = [] rules_arr << :allow_empty_params unless rules.require_inferred_type_params? @@ -486,7 +495,9 @@ def kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kw kwargs.each_pair do |pname, argchain| next unless params.key?(pname.to_s) ptype = params[pname.to_s][:qualified] + ptype = ptype.self_to_type(pin.context) argtype = argchain.infer(api_map, block_pin, locals) + argtype = argtype.self_to_type(block_pin.context) if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 33ec0c4d0..5290c8c12 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -78,7 +78,7 @@ def require_downcasts? rank >= LEVELS[:alpha] end - # We keep this at strong because if you added an @sg-ignore to + # We keep this at strong because if you added an @ sg-ignore to # address a strong-level issue, then ran at a lower level, you'd # get a false positive - we don't run stronger level checks than # requested for performance reasons From 3850399ccf0fd63e5ccaff9a32751a9633bbac59 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 22 Jul 2025 21:16:31 -0400 Subject: [PATCH 025/524] Generic typechecking improvements --- lib/solargraph/complex_type.rb | 7 +++++++ lib/solargraph/complex_type/unique_type.rb | 5 +++++ lib/solargraph/pin/base.rb | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 05e499998..a558f3ba5 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -315,6 +315,13 @@ def all_rooted? all?(&:all_rooted?) end + # @param other [ComplexType, UniqueType] + def erased_version_of?(other) + return false if items.length != 1 || other.items.length != 1 + + @items.first.erased_version_of?(other.items.first) + end + # every top-level type has resolved to be fully qualified; see # #all_rooted? to check their subtypes as well def rooted? diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 361fe06bb..39e5e728f 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -194,6 +194,11 @@ def interface? name.start_with?('_') end + # @param other [UniqueType] + def erased_version_of?(other) + return name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) + end + # @param api_map [ApiMap] # @param expected [ComplexType::UniqueType] # @param situation [:method_call, :return_type] diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index c9e308056..14d324273 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -152,6 +152,10 @@ def combine_return_type(other) other.return_type elsif other.return_type.undefined? return_type + elsif return_type.erased_version_of?(other.return_type) + other.return_type + elsif other.return_type.erased_version_of?(return_type) + 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? From b6b66f397732f43f161c3e7ddc245b5318c34fbe Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 30 Jul 2025 07:59:13 -0400 Subject: [PATCH 026/524] Fix flaky spec --- spec/rbs_map/conversions_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 75ec1c311..d9570ae0f 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -55,7 +55,7 @@ def bar: () -> untyped end context 'with standard loads for solargraph project' do - let(:api_map) { Solargraph::ApiMap.load('.') } + let(:api_map) { Solargraph::ApiMap.load_with_cache('.') } let(:superclass_pin) do api_map.pins.find do |pin| From d506f434e71646b678eb2bfd0a90b637d13e566e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 3 Aug 2025 20:10:19 -0400 Subject: [PATCH 027/524] Linting fixes --- lib/solargraph/api_map.rb | 2 +- lib/solargraph/complex_type.rb | 6 - lib/solargraph/complex_type/type_methods.rb | 7 +- lib/solargraph/complex_type/unique_type.rb | 21 ++- lib/solargraph/type_checker.rb | 5 +- lib/solargraph/type_checker/param_def.rb | 35 ---- spec/complex_type/conforms_to_spec.rb | 179 ++++++++++++-------- spec/source_map/clip_spec.rb | 17 ++ spec/type_checker/levels/alpha_spec.rb | 9 +- spec/type_checker/levels/strict_spec.rb | 2 +- spec/type_checker/levels/strong_spec.rb | 68 +++++--- spec/type_checker/levels/typed_spec.rb | 5 +- 12 files changed, 206 insertions(+), 150 deletions(-) delete mode 100644 lib/solargraph/type_checker/param_def.rb diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 6a0edc5a4..8610e2bc1 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -208,7 +208,7 @@ class << self # @param directory [String] # @param out [IO] The output stream for messages # @return [ApiMap] - def self.load_with_cache directory, out + def self.load_with_cache directory, out = $stdout api_map = load(directory) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index a558f3ba5..8167339e0 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -103,12 +103,6 @@ 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) - atype.conforms_to?(api_map, self, :assignment) - end - # @return [Integer] def length @items.length diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index 791ab80b0..5496f1125 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -73,11 +73,12 @@ def undefined? # @return [Symbol] # @param situation [Symbol] The situation in which the variance is being considered. def erased_variance situation = :method_call - if [:method_call, :return_type, :assignment].include?(situation) - :covariant - else + # :nocov: + unless %i[method_call return_type assignment].include?(situation) raise "Unknown situation: #{situation.inspect}" end + # :nocov: + :covariant end # @param generics_to_erase [Enumerable] diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 39e5e728f..a9f2a899b 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -164,11 +164,11 @@ def ==(other) # # "[Expected] types where neither is possible are INVARIANT" # - # @param situation [:method_call] + # @param _situation [:method_call] # @param default [Symbol] The default variance to return if the type is not one of the special cases # # @return [:invariant, :covariant, :contravariant] - def parameter_variance situation, default = :covariant + def parameter_variance _situation, default = :covariant # @todo RBS can specify variance - maybe we can use that info # and also let folks specify? # @@ -196,7 +196,7 @@ def interface? # @param other [UniqueType] def erased_version_of?(other) - return name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) + name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) end # @param api_map [ApiMap] @@ -257,7 +257,9 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, api_map.super_and_sub?(inferred.name, expected.name) || inferred.name == expected.name else + # :nocov: raise "Unknown erased variance: #{erased_variance.inspect}" + # :nocov: end return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) @@ -302,11 +304,8 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, # @param situation [:method_call, :assignment, :return] # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] # @param variance [:invariant, :covariant, :contravariant] - def conforms_to?(api_map, expected, - situation = :method_call, - rules, + def conforms_to?(api_map, expected, situation, rules, variance:) - return true if undefined? && rules.include?(:allow_undefined) # @todo teach this to validate duck types as inferred type @@ -315,7 +314,11 @@ def conforms_to?(api_map, expected, # complex types as expectations are unions - we only need to # match one of their unique types expected.any? do |expected_unique_type| - raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" unless expected.is_a?(UniqueType) unless expected_unique_type.instance_of?(UniqueType) + # :nocov: + unless expected_unique_type.instance_of?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" + end + # :nocov: conforms_to_unique_type?(api_map, expected_unique_type, situation, rules, variance: variance) end @@ -360,7 +363,7 @@ def to_rbs elsif name.downcase == 'nil' 'nil' elsif name == GENERIC_TAG_NAME - all_params.first.name + all_params.first&.name elsif ['Class', 'Module'].include?(name) rbs_name elsif ['Tuple', 'Array'].include?(name) && fixed_parameters? diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 8b86f02df..3b555bae7 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -5,7 +5,6 @@ module Solargraph # class TypeChecker autoload :Problem, 'solargraph/type_checker/problem' - autoload :ParamDef, 'solargraph/type_checker/param_def' autoload :Rules, 'solargraph/type_checker/rules' include Parser::NodeMethods @@ -100,10 +99,10 @@ def load filename, level = :normal # @param code [String] # @param filename [String, nil] # @param level [Symbol] + # @param api_map [Solargraph::ApiMap] # @return [self] - def load_string code, filename = nil, level = :normal + def load_string code, filename = nil, level = :normal, api_map: Solargraph::ApiMap.new source = Solargraph::Source.load_string(code, filename) - api_map = Solargraph::ApiMap.new api_map.map(source) new(filename, api_map: api_map, level: level) end diff --git a/lib/solargraph/type_checker/param_def.rb b/lib/solargraph/type_checker/param_def.rb deleted file mode 100644 index 2c626270a..000000000 --- a/lib/solargraph/type_checker/param_def.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class TypeChecker - # Data about a method parameter definition. This is the information from - # the args list in the def call, not the `@param` tags. - # - class ParamDef - # @return [String] - attr_reader :name - - # @return [Symbol] - attr_reader :type - - def initialize name, type - @name = name - @type = type - end - - class << self - # Get an array of ParamDefs from a method pin. - # - # @param pin [Solargraph::Pin::Method] - # @return [Array] - def from pin - result = [] - pin.parameters.each do |par| - result.push ParamDef.new(par.name, par.decl) - end - result - end - end - end - end -end diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index 5755721b4..581ce1d34 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -1,20 +1,54 @@ +# frozen_string_literal: true + describe Solargraph::ComplexType do it 'validates simple core types' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String') + exp = described_class.parse('String') + inf = described_class.parse('String') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'invalidates simple core types' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('Integer') + exp = described_class.parse('String') + inf = described_class.parse('Integer') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows subtype skew if told' do + api_map = Solargraph::ApiMap.new + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_subtype_skew]) + expect(match).to be(true) + end + + it 'accepts valid tuple conformance' do + api_map = Solargraph::ApiMap.new + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, Integer)') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'rejects invalid tuple conformance' do + api_map = Solargraph::ApiMap.new + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array(Integer, String)') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(false) end + it 'allows empty params when specified' do + api_map = Solargraph::ApiMap.new + exp = described_class.parse('Array(Integer, Integer)') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) + expect(match).to be(true) + end + it 'validates expected superclasses' do source = Solargraph::Source.load_string(%( class Sup; end @@ -22,32 +56,32 @@ class Sub < Sup; end )) api_map = Solargraph::ApiMap.new api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') + sup = described_class.parse('Sup') + sub = described_class.parse('Sub') match = sub.conforms_to?(api_map, sup, :method_call) expect(match).to be(true) end - it 'invalidates inferred superclasses (expected must be super)' do - # @todo This test might be invalid. There are use cases where inheritance - # between inferred and expected classes should be acceptable in either - # direction. - # source = Solargraph::Source.load_string(%( - # class Sup; end - # class Sub < Sup; end - # )) - # api_map = Solargraph::ApiMap.new - # api_map.map source - # sup = Solargraph::ComplexType.parse('Sup') - # sub = Solargraph::ComplexType.parse('Sub') - # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) - # expect(match).to be(false) - end + # it 'invalidates inferred superclasses (expected must be super)' do + # # @todo This test might be invalid. There are use cases where inheritance + # # between inferred and expected classes should be acceptable in either + # # direction. + # # source = Solargraph::Source.load_string(%( + # # class Sup; end + # # class Sub < Sup; end + # # )) + # # api_map = Solargraph::ApiMap.new + # # api_map.map source + # # sup = described_class.parse('Sup') + # # sub = described_class.parse('Sub') + # # match = Solargraph::TypeChecker::Checks.types_match?(api_map, sub, sup) + # # expect(match).to be(false) + # end it 'fuzzy matches arrays with parameters' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Array') - inf = Solargraph::ComplexType.parse('Array') + exp = described_class.parse('Array') + inf = described_class.parse('Array') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end @@ -57,98 +91,107 @@ class Sub < Sup; end source_map = Solargraph::SourceMap.map(source) api_map = Solargraph::ApiMap.new api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) - exp = Solargraph::ComplexType.parse('Set') - inf = Solargraph::ComplexType.parse('Set') + exp = described_class.parse('Set') + inf = described_class.parse('Set') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'fuzzy matches hashes with parameters' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Hash{ Symbol => String}') - inf = Solargraph::ComplexType.parse('Hash') + exp = described_class.parse('Hash{ Symbol => String}') + inf = described_class.parse('Hash') match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) expect(match).to be(true) end it 'matches multiple types' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('String, Integer') + exp = described_class.parse('String, Integer') + inf = described_class.parse('String, Integer') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'matches multiple types out of order' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String, Integer') - inf = Solargraph::ComplexType.parse('Integer, String') + exp = described_class.parse('String, Integer') + inf = described_class.parse('Integer, String') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'invalidates inferred types missing from expected' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('String') - inf = Solargraph::ComplexType.parse('String, Integer') + exp = described_class.parse('String') + inf = described_class.parse('String, Integer') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(false) end it 'matches nil' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('nil') - inf = Solargraph::ComplexType.parse('nil') + exp = described_class.parse('nil') + inf = described_class.parse('nil') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'validates classes with expected superclasses' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') + exp = described_class.parse('Class') + inf = described_class.parse('Class') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end it 'validates generic classes with expected Class' do api_map = Solargraph::ApiMap.new - exp = Solargraph::ComplexType.parse('Class') - inf = Solargraph::ComplexType.parse('Class') - match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) - expect(match).to be(true) - end - - it 'validates generic classes with expected Class' do - api_map = Solargraph::ApiMap.new - inf = Solargraph::ComplexType.parse('Class') - exp = Solargraph::ComplexType.parse('Class') + inf = described_class.parse('Class') + exp = described_class.parse('Class') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end - it 'validates inheritance in both directions' do - source = Solargraph::Source.load_string(%( - class Sup; end - class Sub < Sup; end - )) - api_map = Solargraph::ApiMap.new - api_map.map source - sup = Solargraph::ComplexType.parse('Sup') - sub = Solargraph::ComplexType.parse('Sub') - match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) - expect(match).to be(true) - match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) - expect(match).to be(true) + context 'with an inheritence relationship' do + let(:source) do + Solargraph::Source.load_string(%( + class Sup; end + class Sub < Sup; end + )) + end + let(:sup) { described_class.parse('Sup') } + let(:sub) { described_class.parse('Sub') } + let(:api_map) { Solargraph::ApiMap.new } + + before do + api_map.map source + end + + it 'validates inheritance in one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end + + it 'validates inheritance the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(true) + end end - it 'invalidates inheritance in both directions' do - api_map = Solargraph::ApiMap.new - sup = Solargraph::ComplexType.parse('String') - sub = Solargraph::ComplexType.parse('Array') - match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) - expect(match).to be(false) - match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) - expect(match).to be(false) + context 'with inheritance relationship in allow_reverse_match mode' do + let(:api_map) { Solargraph::ApiMap.new } + let(:sup) { described_class.parse('String') } + let(:sub) { described_class.parse('Array') } + + it 'conforms one way' do + match = sub.conforms_to?(api_map, sup, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end + + it 'conforms the other way' do + match = sup.conforms_to?(api_map, sub, :method_call, [:allow_reverse_match]) + expect(match).to be(false) + end end end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 0f83331ec..801edefa7 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -302,6 +302,23 @@ def foo expect(type.tag).to eq('String') end + it 'infers method types from return nodes' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [self] + def foo + bar + end + end + Foo.new.foo + ), 'test.rb') + map = Solargraph::ApiMap.new + map.map source + clip = map.clip_at('test.rb', Solargraph::Position.new(7, 10)) + type = clip.infer + expect(type.tag).to eq('Foo') + end + it 'infers multiple method types from return nodes' do source = Solargraph::Source.load_string(%( def foo diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index d700ea3b7..3ff5aa6c3 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + describe Solargraph::TypeChecker do - context 'alpha level' do - def type_checker(code) + context 'when at alpha level' do + def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) end @@ -16,7 +18,8 @@ def bar(b) foo(b) end )) - expect(checker.problems.map(&:message)).to eq(["Wrong argument type for #foo: a expected String, received String, nil"]) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: a expected String, received String, nil']) end end end diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 7861c8817..9992b43e9 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('.', $stdout) + api_map = Solargraph::ApiMap.load '.' 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 diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 4c45056ea..68a62d395 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -14,7 +14,6 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end - it 'ignores nilable type issues' do checker = type_checker(%( # @param a [String] @@ -30,6 +29,54 @@ def bar(b) expect(checker.problems.map(&:message)).to eq([]) end + it 'calls out keyword issues even when required arg count matches' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo('baz') + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + + it 'calls out type issues even when keyword issues are there' do + pending('fixes to arg vs param checking algorithm') + + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)) + .to include('Wrong argument type for #foo: a expected String, received 123') + end + + it 'calls out keyword issues even when arg type issues are there' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + it 'reports missing param tags' do checker = type_checker(%( class Foo @@ -179,23 +226,6 @@ def baz(bing) expect(checker.problems.map(&:message)).to be_empty end - it 'treats a parameter type of undefined as not provided' do - checker = type_checker(%( - class Foo - # @param foo [Class] - # @return [void] - def bar foo:; end - - # @param bing [Class] - # @return [void] - def baz(bing) - bar(foo: bing) - end - end - )) - expect(checker.problems.map(&:message)).to be_empty - end - it 'ignores generic resolution failures' do checker = type_checker(%( class Foo @@ -252,7 +282,7 @@ def block_pins expect(checker.problems.map(&:message)).to be_empty end - it 'ignores generic resolution failures' do + it 'ignores generic resolution failures with only one arg' do checker = type_checker(%( # @generic T # @param path [String] diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index 6e71ee9ff..681d813d5 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -38,7 +38,7 @@ def bar expect(checker.problems.first.message).to include('does not match') end - it 'reports mismatched key and subtypes ' do + it 'reports mismatched key and subtypes' do checker = type_checker(%( # @return [Hash{String => String}] def foo @@ -207,7 +207,8 @@ def foo # @param bar [String] def foo(bar = 123); end )) - expect(checker.problems.map(&:message)).to eq(['Declared type String does not match inferred type 123 for variable bar']) + expect(checker.problems.map(&:message)) + .to eq(['Declared type String does not match inferred type 123 for variable bar']) end it 'validates string default values of parameters' do From fdd3810f34eeaa81e4bbac956d9e8372da1c5c6c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 3 Aug 2025 21:07:30 -0400 Subject: [PATCH 028/524] Linting fix --- lib/solargraph/shell.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 153e77f0e..92f8fed38 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -266,12 +266,8 @@ def method_pin path pins.each do |pin| if options[:typify] || options[:probe] type = ComplexType::UNDEFINED - if options[:typify] - type = pin.typify(api_map) - end - if options[:probe] && type.undefined? - type = pin.probe(api_map) - end + type = pin.typify(api_map) if options[:typify] + type = pin.probe(api_map) if options[:probe] && type.undefined? print_type(type) next end From 30cdfc8b3091ff327c15d06eddb1c96315b2464c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 3 Aug 2025 21:38:42 -0400 Subject: [PATCH 029/524] Add spec --- spec/shell_spec.rb | 114 ++++++++++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 27 +++++++++++ 2 files changed, 141 insertions(+) create mode 100644 spec/shell_spec.rb diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb new file mode 100644 index 000000000..1da2a98a9 --- /dev/null +++ b/spec/shell_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'open3' + +describe Solargraph::Shell do + let(:shell) { described_class.new } + + # @type cmd [Array] + # @return [String] + def bundle_exec(*cmd) + # run the command in the temporary directory with bundle exec + Bundler.with_unbundled_env do + output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}") + expect(status.success?).to be(true), "Command failed: #{output}" + output + end + end + + describe 'method_pin' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(api_map).to receive(:get_path_pins).with('String#to_s').and_return([to_s_pin]) + end + + context 'with no options' do + it 'prints a pin' do + allow(to_s_pin).to receive(:inspect).and_return('pin inspect result') + + out = capture_both { shell.method_pin('String#to_s') } + + expect(out).to eq("pin inspect result\n") + end + end + + context 'with --rbs option' do + it 'prints a pin with RBS type' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + out = capture_both do + shell.options = { rbs: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("pin RBS result\n") + end + end + + context 'with --stack option' do + it 'prints a pin using stack results' do + allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + + allow(api_map).to receive(:get_method_stack).and_return([to_s_pin]) + capture_both do + shell.options = { stack: true } + shell.method_pin('String#to_s') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) + end + + it 'prints a static pin using stack results' do + # allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') + string_new_pin = instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) + + allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) + capture_both do + shell.options = { stack: true } + shell.method_pin('String.new') + end + expect(api_map).to have_received(:get_method_stack).with('String', 'new', scope: :class) + end + end + + context 'with --typify option' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with --typify --rbs options' do + it 'prints a pin with typify type' do + allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) + + out = capture_both do + shell.options = { typify: true, rbs: true } + shell.method_pin('String#to_s') + end + expect(out).to eq("::String\n") + end + end + + context 'with no pin' do + it 'prints error' do + allow(api_map).to receive(:get_path_pins).with('Not#found').and_return([]) + + out = capture_both do + shell.options = {} + shell.method_pin('Not#found') + rescue SystemExit + # Ignore the SystemExit raised by the shell when no pin is found + end + expect(out).to include("Pin not found for path 'Not#found'") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index faba8172e..b69e64097 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,3 +19,30 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end From a30d7904818b16a1a88eafbe9977b9e8598f534d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 3 Aug 2025 21:53:24 -0400 Subject: [PATCH 030/524] Fix spec --- lib/solargraph/shell.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 92f8fed38..76f711552 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -247,7 +247,7 @@ def list # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' # @return [void] def method_pin path - api_map = Solargraph::ApiMap.load_with_cache('.', STDERR) + api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) pins = if options[:stack] scope, ns, meth = if path.include? '#' @@ -260,7 +260,7 @@ def method_pin path api_map.get_path_pins path end if pins.empty? - STDERR.puts "Pin not found for path '#{path}'" + $stderr.puts "Pin not found for path '#{path}'" exit 1 end pins.each do |pin| From 592e6bcd89bd0988e1245c6eac8cd14d2f6b621d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 06:55:28 -0400 Subject: [PATCH 031/524] Speed up spec --- spec/rbs_map/conversions_spec.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index d9570ae0f..008432d68 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -55,21 +55,23 @@ def bar: () -> untyped end context 'with standard loads for solargraph project' do - let(:api_map) { Solargraph::ApiMap.load_with_cache('.') } - - let(:superclass_pin) do - api_map.pins.find do |pin| - pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' - end + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load_with_cache('.') end - it 'finds a superclass pin for Parser::AST::Node' do - expect(superclass_pin).not_to be_nil - end + let(:api_map) { @api_map } # rubocop:disable RSpec/InstanceVariable - it 'generates a rooted pin for superclass of Parser::AST::Node' do - # rooted! - expect(superclass_pin.name) .to eq('::AST::Node') + context 'with superclass pin for Parser::AST::Node' do + let(:superclass_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' + end + end + + it 'generates a rooted pin' do + # rooted! + expect(superclass_pin&.name).to eq('::AST::Node') + end end end end From 14dacc39eba0f1906d2f7f83019d2de3b7217ffa Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 08:04:09 -0400 Subject: [PATCH 032/524] Linting fix --- lib/solargraph/complex_type.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 8167339e0..6203fb5a9 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -224,7 +224,7 @@ def duck_types_match? api_map, expected, inferred raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type? expected.each do |exp| next unless exp.duck_type? - quack = exp.to_s[1..-1] + quack = exp.to_s[1..] return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty? end true From 883574e3077c57fff1e58969ef06aa5ff5f21795 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 08:53:48 -0400 Subject: [PATCH 033/524] Add spec --- spec/complex_type/unique_type_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 spec/complex_type/unique_type_spec.rb diff --git a/spec/complex_type/unique_type_spec.rb b/spec/complex_type/unique_type_spec.rb new file mode 100644 index 000000000..2d9812600 --- /dev/null +++ b/spec/complex_type/unique_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe Solargraph::ComplexType::UniqueType do + describe '#any?' do + let(:type) { described_class.parse('String') } + + it 'yields one and only one type, itself' do + types_encountered = [] + type.any? { |t| types_encountered << t } + expect(types_encountered).to eq([type]) + end + end +end From 637edd0b37caabe007a9539ef8abd4198c73da68 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 09:31:03 -0400 Subject: [PATCH 034/524] Linting fixes --- spec/type_checker/levels/strong_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 68a62d395..1d343c6d9 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -226,7 +226,7 @@ def baz(bing) expect(checker.problems.map(&:message)).to be_empty end - it 'ignores generic resolution failures' do + it 'ignores generic resolution failure with no generic tag' do checker = type_checker(%( class Foo # @param foo [Class] @@ -262,8 +262,7 @@ def block_pins expect(checker.problems.map(&:message)).to be_empty end - - it 'ignores generic resolution failures' do + it 'ignores generic resolution failures from current Solargraph limitation' do checker = type_checker(%( class Foo # @generic T From e8c2cca9cf8728b9b7db04fca1238f2ecc234363 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 11:41:10 -0400 Subject: [PATCH 035/524] Improve test coverage and fix bug --- lib/solargraph/complex_type/unique_type.rb | 4 +- spec/complex_type/conforms_to_spec.rb | 68 ++++++++++++++++------ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index a9f2a899b..13fb7129f 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -241,7 +241,7 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, if variance == :invariant return false unless inferred.name == expected.name - elsif erased_variance == :covariant + elsif variance == :covariant # covariant: we can pass in a more specific type # we contain the expected mix-in, or we have a more specific type @@ -249,7 +249,7 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, api_map.super_and_sub?(expected.name, inferred.name) || inferred.name == expected.name - elsif erased_variance == :contravariant + elsif variance == :contravariant # contravariant: we can pass in a more general type # we contain the expected mix-in, or we have a more general type diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index 581ce1d34..bacf19b5e 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true describe Solargraph::ComplexType do + let(:api_map) do + Solargraph::ApiMap.new + end + it 'validates simple core types' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('String') inf = described_class.parse('String') match = inf.conforms_to?(api_map, exp, :method_call) @@ -10,7 +13,6 @@ end it 'invalidates simple core types' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('String') inf = described_class.parse('Integer') match = inf.conforms_to?(api_map, exp, :method_call) @@ -18,7 +20,6 @@ end it 'allows subtype skew if told' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Array') inf = described_class.parse('Array') match = inf.conforms_to?(api_map, exp, :method_call, [:allow_subtype_skew]) @@ -26,7 +27,6 @@ end it 'accepts valid tuple conformance' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Array(Integer, Integer)') inf = described_class.parse('Array(Integer, Integer)') match = inf.conforms_to?(api_map, exp, :method_call) @@ -34,7 +34,6 @@ end it 'rejects invalid tuple conformance' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Array(Integer, Integer)') inf = described_class.parse('Array(Integer, String)') match = inf.conforms_to?(api_map, exp, :method_call) @@ -42,7 +41,6 @@ end it 'allows empty params when specified' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Array(Integer, Integer)') inf = described_class.parse('Array') match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) @@ -54,7 +52,6 @@ class Sup; end class Sub < Sup; end )) - api_map = Solargraph::ApiMap.new api_map.map source sup = described_class.parse('Sup') sub = described_class.parse('Sub') @@ -70,7 +67,6 @@ class Sub < Sup; end # # class Sup; end # # class Sub < Sup; end # # )) - # # api_map = Solargraph::ApiMap.new # # api_map.map source # # sup = described_class.parse('Sup') # # sub = described_class.parse('Sub') @@ -79,7 +75,6 @@ class Sub < Sup; end # end it 'fuzzy matches arrays with parameters' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Array') inf = described_class.parse('Array') match = inf.conforms_to?(api_map, exp, :method_call) @@ -89,7 +84,6 @@ class Sub < Sup; end it 'fuzzy matches sets with parameters' do source = Solargraph::Source.load_string("require 'set'") source_map = Solargraph::SourceMap.map(source) - api_map = Solargraph::ApiMap.new api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['set']) exp = described_class.parse('Set') inf = described_class.parse('Set') @@ -98,7 +92,6 @@ class Sub < Sup; end end it 'fuzzy matches hashes with parameters' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Hash{ Symbol => String}') inf = described_class.parse('Hash') match = inf.conforms_to?(api_map, exp, :method_call, [:allow_empty_params]) @@ -106,7 +99,6 @@ class Sub < Sup; end end it 'matches multiple types' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('String, Integer') inf = described_class.parse('String, Integer') match = inf.conforms_to?(api_map, exp, :method_call) @@ -114,7 +106,6 @@ class Sub < Sup; end end it 'matches multiple types out of order' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('String, Integer') inf = described_class.parse('Integer, String') match = inf.conforms_to?(api_map, exp, :method_call) @@ -122,7 +113,6 @@ class Sub < Sup; end end it 'invalidates inferred types missing from expected' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('String') inf = described_class.parse('String, Integer') match = inf.conforms_to?(api_map, exp, :method_call) @@ -130,7 +120,6 @@ class Sub < Sup; end end it 'matches nil' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('nil') inf = described_class.parse('nil') match = inf.conforms_to?(api_map, exp, :method_call) @@ -138,7 +127,6 @@ class Sub < Sup; end end it 'validates classes with expected superclasses' do - api_map = Solargraph::ApiMap.new exp = described_class.parse('Class') inf = described_class.parse('Class') match = inf.conforms_to?(api_map, exp, :method_call) @@ -146,13 +134,58 @@ class Sub < Sup; end end it 'validates generic classes with expected Class' do - api_map = Solargraph::ApiMap.new inf = described_class.parse('Class') exp = described_class.parse('Class') match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end + context 'with invariant matching' do + it 'rejects String matching an Object' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'rejects Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(false) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :invariant) + expect(match).to be(true) + end + end + + context 'with contravariant matching' do + it 'rejects String matching an Objet' do + inf = described_class.parse('String') + exp = described_class.parse('Object') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(false) + end + + it 'accepts Object matching an String' do + inf = described_class.parse('Object') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + + it 'accepts String matching a String' do + inf = described_class.parse('String') + exp = described_class.parse('String') + match = inf.conforms_to?(api_map, exp, :method_call, variance: :contravariant) + expect(match).to be(true) + end + end + context 'with an inheritence relationship' do let(:source) do Solargraph::Source.load_string(%( @@ -162,7 +195,6 @@ class Sub < Sup; end end let(:sup) { described_class.parse('Sup') } let(:sub) { described_class.parse('Sub') } - let(:api_map) { Solargraph::ApiMap.new } before do api_map.map source From 13d87eee2fcda17c0fb92e13d14bf82f3c27236c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 12:16:45 -0400 Subject: [PATCH 036/524] Linting fix --- lib/solargraph/complex_type/unique_type.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 13fb7129f..15b7801b1 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -239,9 +239,10 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, return true if inferred == expected - if variance == :invariant + case variance + when :invariant return false unless inferred.name == expected.name - elsif variance == :covariant + when :covariant # covariant: we can pass in a more specific type # we contain the expected mix-in, or we have a more specific type @@ -249,7 +250,7 @@ def conforms_to_unique_type?(api_map, expected, situation = :method_call, api_map.super_and_sub?(expected.name, inferred.name) || inferred.name == expected.name - elsif variance == :contravariant + when :contravariant # contravariant: we can pass in a more general type # we contain the expected mix-in, or we have a more general type From 6f0a151e9f3948679e64abce802806c7061d52bf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 13:05:00 -0400 Subject: [PATCH 037/524] Extract method --- lib/solargraph/complex_type.rb | 1 + lib/solargraph/complex_type/conformance.rb | 133 +++++++++++++++++++++ lib/solargraph/complex_type/unique_type.rb | 106 +--------------- 3 files changed, 137 insertions(+), 103 deletions(-) create mode 100644 lib/solargraph/complex_type/conformance.rb diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 6203fb5a9..d0f46a28d 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -9,6 +9,7 @@ class ComplexType # include TypeMethods include Equality + autoload :Conformance, 'solargraph/complex_type/conformance' autoload :TypeMethods, 'solargraph/complex_type/type_methods' autoload :UniqueType, 'solargraph/complex_type/unique_type' diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb new file mode 100644 index 000000000..f4f37bf4f --- /dev/null +++ b/lib/solargraph/complex_type/conformance.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module Solargraph + class ComplexType + # Checks whether a type can be used in a given situation + class Conformance + # @param api_map [ApiMap] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + # @param situation [:method_call, :return_type] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, + # :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # @param variance [:invariant, :covariant, :contravariant] + def initialize api_map, inferred, expected, + situation = :method_call, rules = [], + variance: inferred.erased_variance(situation) + @api_map = api_map + @inferred = inferred + @expected = expected + @situation = situation + @rules = rules + @variance = variance + unless expected.is_a?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + end + return if inferred.is_a?(UniqueType) + raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" + end + + def conforms_to_unique_type? + unless expected.is_a?(UniqueType) + raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + end + if inferred.literal? && !expected.literal? + conformance = + self.class.new(api_map, inferred.simplify_literals, expected, situation, rules, + variance: variance) + return conformance.conforms_to_unique_type? + end + return true if expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface) + return true if inferred.interface? && rules.include?(:allow_unmatched_interface) + + if rules.include? :allow_reverse_match + reversed_match = expected.conforms_to?(api_map, inferred, situation, + rules - [:allow_reverse_match], + variance: variance) + return true if reversed_match + end + expected = self.expected.downcast_to_literal_if_possible + inferred = self.inferred.downcast_to_literal_if_possible + + if rules.include? :allow_subtype_skew + # parameters are not considered in this case + expected = expected.erase_parameters + end + + inferred = inferred.erase_parameters if !expected.parameters? && inferred.parameters? + + if expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) + expected = expected.erase_parameters + end + + return true if inferred == expected + + case variance + when :invariant + return false unless inferred.name == expected.name + when :covariant + # covariant: we can pass in a more specific type + + # we contain the expected mix-in, or we have a more specific type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(expected.name, inferred.name) || + inferred.name == expected.name + + when :contravariant + # contravariant: we can pass in a more general type + + # we contain the expected mix-in, or we have a more general type + return false unless api_map.type_include?(inferred.name, expected.name) || + api_map.super_and_sub?(inferred.name, expected.name) || + inferred.name == expected.name + else + # :nocov: + raise "Unknown variance: #{variance.inspect}" + # :nocov: + end + + return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) + + # at this point we know the erased type is fine - time to look at parameters + + # there's an implicit 'any' on the expectation parameters + # if there are none specified + return true if expected.all_params.empty? + + unless expected.key_types.empty? + return false if inferred.key_types.empty? + + unless ComplexType.new(inferred.key_types).conforms_to?(api_map, + ComplexType.new(expected.key_types), + situation, + rules, + variance: inferred.parameter_variance(situation)) + return false + end + end + + return true if expected.subtypes.empty? + + return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) + + return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) + + return false if inferred.subtypes.empty? + + ComplexType.new(inferred.subtypes).conforms_to?(api_map, + ComplexType.new(expected.subtypes), + situation, + rules, + variance: inferred.parameter_variance(situation)) + end + + private + + attr_reader :api_map, :inferred, :expected, :situation, :rules, :variance + end + end +end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 15b7801b1..9327d3fc8 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -199,107 +199,6 @@ def erased_version_of?(other) name == other.name && (all_params.empty? || all_params.all?(&:undefined?)) end - # @param api_map [ApiMap] - # @param expected [ComplexType::UniqueType] - # @param situation [:method_call, :return_type] - # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] - # @param variance [:invariant, :covariant, :contravariant] - def conforms_to_unique_type?(api_map, expected, situation = :method_call, - rules = [], - variance: erased_variance(situation)) - raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" unless expected.is_a?(UniqueType) - if literal? && !expected.literal? - return simplify_literals.conforms_to_unique_type?(api_map, expected, situation, - rules, variance: variance) - end - return true if expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface) - return true if interface? && rules.include?(:allow_unmatched_interface) - - if rules.include? :allow_reverse_match - reversed_match = expected.conforms_to?(api_map, self, situation, - rules - [:allow_reverse_match], - variance: variance) - return true if reversed_match - end - expected = expected.downcast_to_literal_if_possible - inferred = downcast_to_literal_if_possible - - if rules.include? :allow_subtype_skew - # parameters are not considered in this case - expected = expected.erase_parameters - end - - if !expected.parameters? && inferred.parameters? - inferred = inferred.erase_parameters - end - - if expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) - expected = expected.erase_parameters - end - - return true if inferred == expected - - case variance - when :invariant - return false unless inferred.name == expected.name - when :covariant - # covariant: we can pass in a more specific type - - # we contain the expected mix-in, or we have a more specific type - return false unless api_map.type_include?(inferred.name, expected.name) || - api_map.super_and_sub?(expected.name, inferred.name) || - inferred.name == expected.name - - when :contravariant - # contravariant: we can pass in a more general type - - # we contain the expected mix-in, or we have a more general type - return false unless api_map.type_include?(inferred.name, expected.name) || - api_map.super_and_sub?(inferred.name, expected.name) || - inferred.name == expected.name - else - # :nocov: - raise "Unknown erased variance: #{erased_variance.inspect}" - # :nocov: - end - - return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) - - # at this point we know the erased type is fine - time to look at parameters - - # there's an implicit 'any' on the expectation parameters - # if there are none specified - return true if expected.all_params.empty? - - unless expected.key_types.empty? - return false if inferred.key_types.empty? - - return false unless ComplexType.new(inferred.key_types).conforms_to?(api_map, - ComplexType.new(expected.key_types), - situation, - rules, - variance: parameter_variance(situation)) - end - - return true if expected.subtypes.empty? - - return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) - - return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) - - return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) - - return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic) - - return false if inferred.subtypes.empty? - - ComplexType.new(inferred.subtypes).conforms_to?(api_map, - ComplexType.new(expected.subtypes), - situation, - rules, - variance: parameter_variance(situation)) - end - # @param api_map [ApiMap] # @param expected [ComplexType::UniqueType, ComplexType] # @param situation [:method_call, :assignment, :return] @@ -320,8 +219,9 @@ def conforms_to?(api_map, expected, situation, rules, raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}" end # :nocov: - conforms_to_unique_type?(api_map, expected_unique_type, situation, - rules, variance: variance) + conformance = Conformance.new(api_map, self, expected_unique_type, situation, + rules, variance: variance) + conformance.conforms_to_unique_type? end end From f9a493206a71882eb4603fcdd1055c957cbd91eb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 13:30:57 -0400 Subject: [PATCH 038/524] Refactors --- lib/solargraph/complex_type/conformance.rb | 31 ++++++++++++++-------- lib/solargraph/complex_type/unique_type.rb | 4 +++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index f4f37bf4f..a055c297c 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -31,11 +31,8 @@ def conforms_to_unique_type? unless expected.is_a?(UniqueType) raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" end - if inferred.literal? && !expected.literal? - conformance = - self.class.new(api_map, inferred.simplify_literals, expected, situation, rules, - variance: variance) - return conformance.conforms_to_unique_type? + if inferred.simplifyable_literal? && !expected.literal? + return with_new_types(inferred.simplify_literals, expected).conforms_to_unique_type? end return true if expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface) return true if inferred.interface? && rules.include?(:allow_unmatched_interface) @@ -46,18 +43,23 @@ def conforms_to_unique_type? variance: variance) return true if reversed_match end - expected = self.expected.downcast_to_literal_if_possible - inferred = self.inferred.downcast_to_literal_if_possible + if expected != expected.downcast_to_literal_if_possible || + inferred != inferred.downcast_to_literal_if_possible + return with_new_types(inferred.downcast_to_literal_if_possible, + expected.downcast_to_literal_if_possible).conforms_to_unique_type? + end - if rules.include? :allow_subtype_skew + if rules.include?(:allow_subtype_skew) && !expected.parameters.empty? # parameters are not considered in this case - expected = expected.erase_parameters + return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? end - inferred = inferred.erase_parameters if !expected.parameters? && inferred.parameters? + if !expected.parameters? && inferred.parameters? + return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? + end if expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) - expected = expected.erase_parameters + return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? end return true if inferred == expected @@ -127,6 +129,13 @@ def conforms_to_unique_type? private + # @return [self] + # @param inferred [ComplexType::UniqueType] + # @param expected [ComplexType::UniqueType] + def with_new_types inferred, expected + self.class.new(api_map, inferred, expected, situation, rules, variance: variance) + end + attr_reader :api_map, :inferred, :expected, :situation, :rules, :variance end end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 9327d3fc8..b12ca6332 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -113,6 +113,10 @@ def simplify_literals end end + def simplifyable_literal? + literal? && name != 'nil' + end + def literal? non_literal_name != name end From bc71924f40e706990a2ad90973e13eba78c35342 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 13:32:26 -0400 Subject: [PATCH 039/524] Coverage fixes --- lib/solargraph/complex_type/conformance.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index a055c297c..8d4b9ec3b 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -20,11 +20,14 @@ def initialize api_map, inferred, expected, @situation = situation @rules = rules @variance = variance + # :nocov: unless expected.is_a?(UniqueType) raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" end return if inferred.is_a?(UniqueType) + # :nocov: raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" + # :nocov: end def conforms_to_unique_type? From 18bbe8a81045138d619d5abda8c4f7ec1477cb9c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 13:33:27 -0400 Subject: [PATCH 040/524] Coverage fixes --- lib/solargraph/complex_type/conformance.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index 8d4b9ec3b..888d53ec2 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -31,9 +31,11 @@ def initialize api_map, inferred, expected, end def conforms_to_unique_type? + # :nocov: unless expected.is_a?(UniqueType) raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" end + # :nocov: if inferred.simplifyable_literal? && !expected.literal? return with_new_types(inferred.simplify_literals, expected).conforms_to_unique_type? end From dc50122896e1923242487964b4e3b07ec08903a5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 15:04:28 -0400 Subject: [PATCH 041/524] Refactor --- lib/solargraph/complex_type/conformance.rb | 122 +++++++++++++-------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index 888d53ec2..6a3d128a7 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -9,7 +9,8 @@ class Conformance # @param expected [ComplexType::UniqueType] # @param situation [:method_call, :return_type] # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, - # :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] + # :allow_any_match, :allow_undefined, :allow_unresolved_generic, + # :allow_unmatched_interface>] # @param variance [:invariant, :covariant, :contravariant] def initialize api_map, inferred, expected, situation = :method_call, rules = [], @@ -33,56 +34,92 @@ def initialize api_map, inferred, expected, def conforms_to_unique_type? # :nocov: unless expected.is_a?(UniqueType) + # :nocov: raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" + # :nocov: end - # :nocov: - if inferred.simplifyable_literal? && !expected.literal? + + if use_simplified_inferred_type? return with_new_types(inferred.simplify_literals, expected).conforms_to_unique_type? end - return true if expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface) - return true if inferred.interface? && rules.include?(:allow_unmatched_interface) - - if rules.include? :allow_reverse_match - reversed_match = expected.conforms_to?(api_map, inferred, situation, - rules - [:allow_reverse_match], - variance: variance) - return true if reversed_match - end - if expected != expected.downcast_to_literal_if_possible || - inferred != inferred.downcast_to_literal_if_possible - return with_new_types(inferred.downcast_to_literal_if_possible, - expected.downcast_to_literal_if_possible).conforms_to_unique_type? + return true if ignore_interface? + return true if conforms_via_reverse_match? + + downcast_inferred = inferred.downcast_to_literal_if_possible + downcast_expected = expected.downcast_to_literal_if_possible + if (downcast_inferred.name != inferred.name) || (downcast_expected.name != expected.name) + return with_new_types(downcast_inferred, downcast_expected).conforms_to_unique_type? end - if rules.include?(:allow_subtype_skew) && !expected.parameters.empty? + if rules.include?(:allow_subtype_skew) && !expected.all_params.empty? # parameters are not considered in this case return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? end - if !expected.parameters? && inferred.parameters? - return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? - end + return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? if only_inferred_parameters? - if expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) - return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? - end + return conforms_via_stripped_expected_parameters? if can_strip_expected_parameters? return true if inferred == expected + return false unless erased_type_conforms? + + return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) + + # at this point we know the erased type is fine - time to look at parameters + + # there's an implicit 'any' on the expectation parameters + # if there are none specified + return true if expected.all_params.empty? + + return false unless key_types_conform? + + subtypes_conform? + end + + private + + def use_simplified_inferred_type? + inferred.simplifyable_literal? && !expected.literal? + end + + def only_inferred_parameters? + !expected.parameters? && inferred.parameters? + end + + def conforms_via_stripped_expected_parameters? + with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type? + end + + def ignore_interface? + (expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface)) || + (inferred.interface? && rules.include?(:allow_unmatched_interface)) + end + + def can_strip_expected_parameters? + expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params) + end + + def conforms_via_reverse_match? + return false unless rules.include? :allow_reverse_match + + expected.conforms_to?(api_map, inferred, situation, + rules - [:allow_reverse_match], + variance: variance) + end + + def erased_type_conforms? case variance when :invariant return false unless inferred.name == expected.name when :covariant # covariant: we can pass in a more specific type - # we contain the expected mix-in, or we have a more specific type return false unless api_map.type_include?(inferred.name, expected.name) || api_map.super_and_sub?(expected.name, inferred.name) || inferred.name == expected.name - when :contravariant # contravariant: we can pass in a more general type - # we contain the expected mix-in, or we have a more general type return false unless api_map.type_include?(inferred.name, expected.name) || api_map.super_and_sub?(inferred.name, expected.name) || @@ -92,27 +129,26 @@ def conforms_to_unique_type? raise "Unknown variance: #{variance.inspect}" # :nocov: end + true + end - return true if inferred.all_params.empty? && rules.include?(:allow_empty_params) - - # at this point we know the erased type is fine - time to look at parameters - - # there's an implicit 'any' on the expectation parameters - # if there are none specified - return true if expected.all_params.empty? + def key_types_conform? + return true if expected.key_types.empty? - unless expected.key_types.empty? - return false if inferred.key_types.empty? + return false if inferred.key_types.empty? - unless ComplexType.new(inferred.key_types).conforms_to?(api_map, - ComplexType.new(expected.key_types), - situation, - rules, - variance: inferred.parameter_variance(situation)) - return false - end + unless ComplexType.new(inferred.key_types).conforms_to?(api_map, + ComplexType.new(expected.key_types), + situation, + rules, + variance: inferred.parameter_variance(situation)) + return false end + true + end + + def subtypes_conform? return true if expected.subtypes.empty? return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined) @@ -132,8 +168,6 @@ def conforms_to_unique_type? variance: inferred.parameter_variance(situation)) end - private - # @return [self] # @param inferred [ComplexType::UniqueType] # @param expected [ComplexType::UniqueType] From 7fcd43a9d705cf0e30b4c590575ec29a6f2c4ff5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 15:36:15 -0400 Subject: [PATCH 042/524] Fix nocov markers --- lib/solargraph/complex_type/conformance.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index 6a3d128a7..424413038 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -25,6 +25,7 @@ def initialize api_map, inferred, expected, unless expected.is_a?(UniqueType) raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" end + # :nocov: return if inferred.is_a?(UniqueType) # :nocov: raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}" @@ -32,7 +33,6 @@ def initialize api_map, inferred, expected, end def conforms_to_unique_type? - # :nocov: unless expected.is_a?(UniqueType) # :nocov: raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}" From bb77106490afd84e82e5660ec8067f12473f65a8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 16:52:23 -0400 Subject: [PATCH 043/524] Add another spec, fix behavior --- lib/solargraph/complex_type/unique_type.rb | 4 ++-- spec/complex_type/conforms_to_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index b12ca6332..c009a08d9 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -208,8 +208,8 @@ def erased_version_of?(other) # @param situation [:method_call, :assignment, :return] # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] # @param variance [:invariant, :covariant, :contravariant] - def conforms_to?(api_map, expected, situation, rules, - variance:) + def conforms_to?(api_map, expected, situation, rules = [], + variance: erased_variance(situation)) return true if undefined? && rules.include?(:allow_undefined) # @todo teach this to validate duck types as inferred type diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index bacf19b5e..c1bba05dc 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -56,6 +56,12 @@ class Sub < Sup; end sup = described_class.parse('Sup') sub = described_class.parse('Sub') match = sub.conforms_to?(api_map, sup, :method_call) + end + + it 'handles singleton types compared against their literals' do + exp = Solargraph::ComplexType::UniqueType.new('nil', rooted: true) + inf = Solargraph::ComplexType::UniqueType.new('NilClass', rooted: true) + match = inf.conforms_to?(api_map, exp, :method_call) expect(match).to be(true) end From ed69326afb742f37e78ce475ed38b40f93ece405 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 16:59:12 -0400 Subject: [PATCH 044/524] Fix merge issue --- spec/complex_type/conforms_to_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index c1bba05dc..4ac00e70a 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -56,6 +56,7 @@ class Sub < Sup; end sup = described_class.parse('Sup') sub = described_class.parse('Sub') match = sub.conforms_to?(api_map, sup, :method_call) + expect(match).to be(true) end it 'handles singleton types compared against their literals' do From 1e7c972f314003184ac0a43828986f27fcc4aa32 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 4 Aug 2025 17:13:30 -0400 Subject: [PATCH 045/524] More specs --- spec/complex_type/conforms_to_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/complex_type/conforms_to_spec.rb b/spec/complex_type/conforms_to_spec.rb index 4ac00e70a..f8a623bf0 100644 --- a/spec/complex_type/conforms_to_spec.rb +++ b/spec/complex_type/conforms_to_spec.rb @@ -26,6 +26,27 @@ expect(match).to be(true) end + it 'allows covariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + + it 'does not allow contravariant behavior in parameters of Array' do + exp = described_class.parse('Array') + inf = described_class.parse('Array') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(false) + end + + it 'allows covariant behavior in key types of Hash' do + exp = described_class.parse('Hash{Object => String}') + inf = described_class.parse('Hash{Integer => String}') + match = inf.conforms_to?(api_map, exp, :method_call) + expect(match).to be(true) + end + it 'accepts valid tuple conformance' do exp = described_class.parse('Array(Integer, Integer)') inf = described_class.parse('Array(Integer, Integer)') From a33381bddc82dfba278193f0b31b27dc988dd89c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 11:50:00 -0400 Subject: [PATCH 046/524] Linting fixes --- lib/solargraph/api_map.rb | 14 -------------- lib/solargraph/gem_pins.rb | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index a49ac0a0d..c79c30f60 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -920,20 +920,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) - if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) - Solargraph.assert_or_log(:alias_visibility) { "Rejecting alias - visibility of target is #{resolved.visibility}, looking for visibility #{visibility}" } - next pin - end - resolved - end.compact - end - # @param pin [Pin::MethodAlias, Pin::Base] # @return [Pin::Method] def resolve_method_alias pin diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 4bfc1a6d5..fc86a0d92 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -49,7 +49,7 @@ def self.build_yard_pins(yard_plugins, gemspec) 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 From 5caa11e79c49f9847996dfa597d6b232b7250a5a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 12:17:58 -0400 Subject: [PATCH 047/524] Ratchet rubocop todo --- .rubocop_todo.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8283c5c6..57b773d40 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -571,7 +571,6 @@ Lint/NonAtomicFileOperation: # This cop supports safe autocorrection (--autocorrect). Lint/ParenthesesAsGroupedExpression: Exclude: - - 'lib/solargraph.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'spec/language_server/host_spec.rb' - 'spec/source_map/clip_spec.rb' @@ -639,7 +638,6 @@ Lint/UnusedBlockArgument: # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: - - 'lib/solargraph.rb' - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/convention/base.rb' - 'lib/solargraph/diagnostics/base.rb' @@ -2135,13 +2133,6 @@ Style/NegatedIfElseCondition: - 'lib/solargraph/shell.rb' - 'lib/solargraph/type_checker.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods. -# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Exclude: - - 'lib/solargraph/type_checker.rb' - # This cop supports safe autocorrection (--autocorrect). Style/NestedTernaryOperator: Exclude: @@ -2237,7 +2228,6 @@ Style/RedundantBegin: - 'lib/solargraph/shell.rb' - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/workspace.rb' # This cop supports safe autocorrection (--autocorrect). @@ -2653,7 +2643,6 @@ YARD/MismatchName: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/language_server/request.rb' From c2aac2b6c41411d4004538514adfebf7cb96f80f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 13:29:51 -0400 Subject: [PATCH 048/524] Fix undercover issue --- lib/solargraph/api_map.rb | 2 ++ spec/pin/method_alias_spec.rb | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 spec/pin/method_alias_spec.rb diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index c79c30f60..23f10b116 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -929,8 +929,10 @@ def resolve_method_alias pin origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first @method_alias_stack.pop if origin.nil? + # :nocov: Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{pin.full_context.tag} #{pin.original}() in #{pin.scope} scope = #{pin.inspect}" } return nil + # :nocov: end args = { location: pin.location, diff --git a/spec/pin/method_alias_spec.rb b/spec/pin/method_alias_spec.rb new file mode 100644 index 000000000..d3b408273 --- /dev/null +++ b/spec/pin/method_alias_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +describe Solargraph::Pin::MethodAlias do + describe '#to_rbs' do + it 'generates RBS from simple alias' do + method_alias = described_class.new(name: 'name', original: 'original_name') + + expect(method_alias.to_rbs).to eq('alias name original_name') + end + + it 'generates RBS from static alias' do + method_alias = described_class.new(name: 'name', original: 'original_name', scope: :class) + + expect(method_alias.to_rbs).to eq('alias self.name self.original_name') + end + end +end From a45bc0ac4dad40a7747e737d7c14ccade748b3e3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 14:05:31 -0400 Subject: [PATCH 049/524] Fix linting issues --- .rubocop.yml | 3 ++ .rubocop_todo.yml | 97 ----------------------------------------------- 2 files changed, 3 insertions(+), 97 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..185cd8955 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,6 +39,9 @@ Metrics/ParameterLists: Style/ClassVars: Enabled: false +RSpec/SpecFilePathFormat: + Enabled: false + # # Set a relaxed standard on harder-to-address item for ease of # contribution - if you are good at refactoring, we welcome PRs to diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 57b773d40..06bc41596 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1278,103 +1278,6 @@ RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/language_server/host/message_worker_spec.rb' From 964b2d8d7a2802ed57f96a987105f3bdd8ebe72f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 15:50:58 -0400 Subject: [PATCH 050/524] Fix merge --- 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 714ca20c8..54f2ab449 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -1028,11 +1028,11 @@ def resolve_method_alias(alias_pin) end if original.nil? # :nocov: - Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{pin.full_context.tag} #{pin.original}() in #{pin.scope} scope = #{pin.inspect}" } + Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{alias_pin.full_context.tag} #{alias_pin.original} in #{alias_pin.scope} scope = #{alias_pin.inspect}" } return nil # :nocov: end - + # @sg-ignore ignore `received nil` for original create_resolved_alias_pin(alias_pin, original) end From 9b2ddf6b38865d04266fd093b146f26f72fecf50 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 15:52:07 -0400 Subject: [PATCH 051/524] Bring in new RuboCop version to todo file --- .rubocop_todo.yml | 132 ++++------------------------------------------ 1 file changed, 11 insertions(+), 121 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5f20e3d56..cfa4dd37e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,51 +1,44 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.2. +# using RuboCop version 1.80.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: **/*.gemspec Gemspec/AddRuntimeDependency: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' -# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# Configuration parameters: EnforcedStyle, AllowedGems. # SupportedStyles: Gemfile, gems.rb, gemspec -# Include: **/*.gemspec, **/Gemfile, **/gems.rb Gemspec/DevelopmentDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemspec +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. Gemspec/OrderedDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequiredRubyVersion: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' @@ -622,7 +615,7 @@ Lint/UnmodifiedReduceAccumulator: - 'lib/solargraph/pin/method.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' @@ -630,7 +623,7 @@ Lint/UnusedBlockArgument: - 'spec/language_server/transport/data_reader_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: @@ -659,13 +652,12 @@ Lint/UnusedMethodArgument: - 'spec/doc_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. Lint/UselessAccessModifier: Exclude: - 'lib/solargraph/api_map.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - 'lib/solargraph/doc_map.rb' @@ -696,7 +688,6 @@ Lint/UselessConstantScoping: - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. Lint/UselessMethodDefinition: Exclude: - 'lib/solargraph/pin/signature.rb' @@ -1008,7 +999,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. RSpec/EmptyExampleGroup: Exclude: - 'spec/convention_spec.rb' @@ -1108,7 +1098,6 @@ RSpec/LeakyConstantDeclaration: - 'spec/complex_type_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' @@ -1266,109 +1255,10 @@ RSpec/RepeatedExample: - 'spec/type_checker/levels/strict_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/api_map_method_spec.rb' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/language_server/host/message_worker_spec.rb' @@ -1702,7 +1592,7 @@ Style/EmptyLambdaParameter: - 'spec/rbs_map/core_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: @@ -2233,7 +2123,7 @@ Style/RedundantFreeze: - 'lib/solargraph/source_map/mapper.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, AllowComments. +# Configuration parameters: AllowComments. Style/RedundantInitialize: Exclude: - 'lib/solargraph/rbs_map/core_map.rb' From c5acdfc8aec9677af0513598b602556758070f50 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 16:50:57 -0400 Subject: [PATCH 052/524] Resolve aliases to .new pins --- lib/solargraph/api_map.rb | 10 +++++++--- spec/api_map/api_map_method_spec.rb | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 54f2ab449..962d8e8cd 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -1005,13 +1005,17 @@ def prefer_non_nil_variables pins # @param alias_pin [Pin::MethodAlias] # @return [Pin::Method, nil] def resolve_method_alias(alias_pin) - ancestors = store.get_ancestors(alias_pin.full_context.tag) + ancestors = store.get_ancestors(alias_pin.full_context.namespace) original = nil # Search each ancestor for the original method ancestors.each do |ancestor_fqns| ancestor_fqns = ComplexType.parse(ancestor_fqns).force_rooted.namespace - ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}" + ancestor_method_path = if alias_pin.original == 'new' && alias_pin.scope == :class + "#{ancestor_fqns}#initialize" + else + "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}" + end # Search for the original method in the ancestor original = store.get_path_pins(ancestor_method_path).find do |candidate_pin| @@ -1021,7 +1025,7 @@ def resolve_method_alias(alias_pin) break resolved if resolved end - candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope + candidate_pin.is_a?(Pin::Method) end break if original diff --git a/spec/api_map/api_map_method_spec.rb b/spec/api_map/api_map_method_spec.rb index a3adc9b94..c599768d8 100644 --- a/spec/api_map/api_map_method_spec.rb +++ b/spec/api_map/api_map_method_spec.rb @@ -11,6 +11,13 @@ api_map.catalog bench end + describe '#resolve_method_alias' do + it 'resolves the IO.for_fd alias to IO.new' do + stack = api_map.get_method_stack('IO', 'for_fd', scope: :class) + expect(stack).not_to be_empty + end + end + describe '#qualify' do let(:external_requires) { ['yaml'] } From 14b794b6b7b6026d81300e9e4b85545f0d01cfd2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 17:00:54 -0400 Subject: [PATCH 053/524] Improve spec --- spec/api_map/api_map_method_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/api_map/api_map_method_spec.rb b/spec/api_map/api_map_method_spec.rb index c599768d8..0ac32d608 100644 --- a/spec/api_map/api_map_method_spec.rb +++ b/spec/api_map/api_map_method_spec.rb @@ -14,7 +14,7 @@ describe '#resolve_method_alias' do it 'resolves the IO.for_fd alias to IO.new' do stack = api_map.get_method_stack('IO', 'for_fd', scope: :class) - expect(stack).not_to be_empty + expect(stack.map(&:class).uniq).to eq([Solargraph::Pin::Method]) end end From 5799b5213e8d7d6d2f3d680d7010367a1fc03956 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 09:00:00 -0400 Subject: [PATCH 054/524] Lint fixes --- lib/solargraph/api_map.rb | 7 ++++--- spec/source/chain/call_spec.rb | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 7ed8636e4..205ae4c33 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -62,7 +62,6 @@ def hash equality_fields.hash end - attr_reader :loose_unions def to_s @@ -192,6 +191,7 @@ def clip_at filename, position # Create an ApiMap with a workspace in the specified directory. # # @param directory [String] + # @param loose_unions [Boolean] See #initialize # @return [ApiMap] def self.load directory, loose_unions: true api_map = new(loose_unions: loose_unions) @@ -226,9 +226,10 @@ class << self # # # @param directory [String] - # @param out [IO] The output stream for messages + # @param out [IO, nil] The output stream for messages + # @param loose_unions [Boolean] See #initialize # @return [ApiMap] - def self.load_with_cache directory, out = IO::NULL, loose_unions: true + def self.load_with_cache directory, out = nil, loose_unions: true api_map = load(directory, loose_unions: loose_unions) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index ab22eca44..aac486605 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -407,7 +407,6 @@ def bar; end expect(type.tag).to eq('String') end - it 'denies calls off of nilable objects when loose union mode is off' do source = Solargraph::Source.load_string(%( # @type [String, nil] From e4afaada19c69de9dfc574ae3bcfc66880a44be9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 08:21:34 -0400 Subject: [PATCH 055/524] Allow newer RBS gem versions, exclude incompatible ones (#995) * Allow newer RBS gem versions This allow users to upgrade to recent Tapioca versions. Tapioca now requires newish versions of the spoom gem, which depends on 4.x pre-releases of the rbs gem. * Add RBS version to test matrix * Add exclude rule * Move to 3.6.1 --- .github/workflows/rspec.yml | 10 +++++++++- solargraph.gemspec | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 94ef5771c..35f7a1d13 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -22,7 +22,13 @@ jobs: strategy: matrix: ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', 'head'] - + rbs-version: ['3.6.1', '3.9.4', '4.0.0.dev.4'] + # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 + exclude: + - ruby-version: '3.0' + rbs-version: '3.9.4' + - ruby-version: '3.0' + rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby @@ -30,6 +36,8 @@ jobs: with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: false + - name: Set rbs version + run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile # /home/runner/.rubies/ruby-head/lib/ruby/gems/3.5.0+2/gems/rbs-3.9.4/lib/rbs.rb:11: # warning: tsort was loaded from the standard library, # but will no longer be part of the default gems diff --git a/solargraph.gemspec b/solargraph.gemspec index e4d30c537..e6bb9394a 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| 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.6.1' + s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4'] s.add_runtime_dependency 'reverse_markdown', '~> 3.0' s.add_runtime_dependency 'rubocop', '~> 1.76' s.add_runtime_dependency 'thor', '~> 1.0' From a0a78785c23a2fa2f65865192fab961ee8a9a751 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 08:55:48 -0400 Subject: [PATCH 056/524] Look for external requires before cataloging bench (#1021) * Look for external requires before cataloging bench It seems like sync_catalog will go through the motions but not actually load pins from gems here due to passing an empty requires array to ApiMap. I'm sure those requires get pulled in eventually, but we go through at least one catalog cycle without it happening. Found while trying to test a different issue but not being able to get completions from a gem in a spec. * Ensure backport is pre-cached --- lib/solargraph/library.rb | 2 +- spec/library_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 0316a01b0..9eb171879 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -680,8 +680,8 @@ def sync_catalog mutex.synchronize do logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}" - api_map.catalog bench source_map_hash.values.each { |map| find_external_requires(map) } + api_map.catalog bench logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" diff --git a/spec/library_spec.rb b/spec/library_spec.rb index bd7cc25a0..bea0f2983 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -26,6 +26,28 @@ expect(completion.pins.map(&:name)).to include('x') end + context 'with a require from an already-cached external gem' do + before do + Solargraph::Shell.new.gems('backport') + end + + it "returns a Completion" do + library = Solargraph::Library.new(Solargraph::Workspace.new(Dir.pwd, + Solargraph::Workspace::Config.new)) + library.attach Solargraph::Source.load_string(%( + require 'backport' + + # @param adapter [Backport::Adapter] + def foo(adapter) + adapter.remo + end + ), 'file.rb', 0) + completion = library.completions_at('file.rb', 5, 19) + expect(completion).to be_a(Solargraph::SourceMap::Completion) + expect(completion.pins.map(&:name)).to include('remote') + end + end + it "gets definitions from a file" do library = Solargraph::Library.new src = Solargraph::Source.load_string %( From 0e86b883ee16ac764ed41f24e424a85954137736 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 19 Aug 2025 10:01:27 -0400 Subject: [PATCH 057/524] Remove Library#folding_ranges (#904) --- lib/solargraph/library.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 9eb171879..72224f672 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -437,17 +437,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 c90f01684972682abfbd21da124f3d6c7b37f9a1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 19 Aug 2025 10:29:27 -0400 Subject: [PATCH 058/524] Complain in strong type-checking if an @sg-ignore line is not needed (#1011) * Complain in strong type-checking if an @sg-ignore line is not needed * Fix return type * Fix spec * Linting/coverage fixes * Coverage fix --- lib/solargraph/api_map.rb | 3 - lib/solargraph/complex_type.rb | 1 - lib/solargraph/complex_type/unique_type.rb | 4 -- lib/solargraph/parser/comment_ripper.rb | 2 +- .../parser/flow_sensitive_typing.rb | 2 - .../parser/parser_gem/class_methods.rb | 2 +- lib/solargraph/pin/base.rb | 2 - lib/solargraph/pin/base_variable.rb | 1 - lib/solargraph/pin/method.rb | 1 - lib/solargraph/source.rb | 3 +- lib/solargraph/source/cursor.rb | 1 - lib/solargraph/type_checker.rb | 64 +++++++++++++++---- lib/solargraph/type_checker/rules.rb | 8 +++ lib/solargraph/workspace/config.rb | 2 - spec/type_checker/levels/strong_spec.rb | 11 ++++ 15 files changed, 75 insertions(+), 32 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 9db37a166..eed02b4ef 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -211,9 +211,6 @@ class << self # any missing gems. # # - # @todo IO::NULL is incorrectly inferred to be a String. - # @sg-ignore - # # @param directory [String] # @param out [IO] The output stream for messages # @return [ApiMap] diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 9e23eb502..ac9599329 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -299,7 +299,6 @@ class << self # # @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 diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 0f4ec430d..63a6ae15b 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -49,11 +49,7 @@ def self.parse name, substring = '', make_rooted: nil parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0]) if parameters_type == :hash 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 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} diff --git a/lib/solargraph/parser/comment_ripper.rb b/lib/solargraph/parser/comment_ripper.rb index 62a4dacc5..e74fcb259 100644 --- a/lib/solargraph/parser/comment_ripper.rb +++ b/lib/solargraph/parser/comment_ripper.rb @@ -51,7 +51,7 @@ def on_embdoc_end *args result end - # @return [Hash{Integer => String}] + # @return [Hash{Integer => Solargraph::Parser::Snippet}] def parse @comments = {} super diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 58f149d73..308db214b 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -236,8 +236,6 @@ def type_name(node) "#{module_type_name}::#{class_node}" end - # @todo "return type could not be inferred" should not trigger here - # @sg-ignore # @param clause_node [Parser::AST::Node] def always_breaks?(clause_node) clause_node&.type == :break diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 58ca8056b..ddc742bd8 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -17,7 +17,7 @@ module ParserGem module ClassMethods # @param code [String] # @param filename [String, nil] - # @return [Array(Parser::AST::Node, Hash{Integer => String})] + # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})] def parse_with_comments code, filename = nil node = parse(code, filename) comments = CommentRipper.new(code, filename, 0).parse diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 6ac2cac52..fb3274dab 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -129,7 +129,6 @@ def choose_longer(other, attr) val2 = other.send(attr) return val1 if val1 == val2 return val2 if val1.nil? - # @sg-ignore val1.length > val2.length ? val1 : val2 end @@ -268,7 +267,6 @@ def assert_same_array_content(other, attr, &block) 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] diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index cef3f44cb..764c1fb39 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -43,7 +43,6 @@ 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? diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 6302a940a..6309cb55a 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -305,7 +305,6 @@ def typify api_map super end - # @sg-ignore def documentation if @documentation.nil? method_docs ||= super || '' diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index 11ab215ed..d2b24cc61 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -30,7 +30,7 @@ def node @node end - # @return [Hash{Integer => Array}] + # @return [Hash{Integer => Solargraph::Parser::Snippet}] def comments finalize @comments @@ -235,6 +235,7 @@ def synchronized? # @return [Hash{Integer => String}] def associated_comments @associated_comments ||= begin + # @type [Hash{Integer => String}] result = {} buffer = String.new('') # @type [Integer, nil] diff --git a/lib/solargraph/source/cursor.rb b/lib/solargraph/source/cursor.rb index 0b95bb9bd..70e4fd47a 100644 --- a/lib/solargraph/source/cursor.rb +++ b/lib/solargraph/source/cursor.rb @@ -35,7 +35,6 @@ def word # The part of the word before the current position. Given the text # `foo.bar`, the start_of_word at position(0, 6) is `ba`. # - # @sg-ignore Improve resolution of String#match below # @return [String] def start_of_word @start_of_word ||= begin diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index aa215f97b..e99f99195 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -38,15 +38,20 @@ def source_map @source_map ||= api_map.source_map(filename) end + # @return [Source] + def source + @source_map.source + end + # @return [Array] def problems @problems ||= begin - without_ignored( - method_tag_problems - .concat variable_type_tag_problems - .concat const_problems - .concat call_problems - ) + all = method_tag_problems + .concat(variable_type_tag_problems) + .concat(const_problems) + .concat(call_problems) + unignored = without_ignored(all) + unignored.concat(unneeded_sgignore_problems) end end @@ -140,7 +145,7 @@ def resolved_constant? pin # @param pin [Pin::Base] def virtual_pin? pin - pin.location && source_map.source.comment_at?(pin.location.range.ending) + pin.location && source.comment_at?(pin.location.range.ending) end # @param pin [Pin::Method] @@ -231,7 +236,7 @@ def all_variables def const_problems return [] unless rules.validate_consts? result = [] - Solargraph::Parser::NodeMethods.const_nodes_from(source_map.source.node).each do |const| + Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const| rng = Solargraph::Range.from_node(const) chain = Solargraph::Parser.chain(const, filename) block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column) @@ -249,7 +254,7 @@ def const_problems # @return [Array] def call_problems result = [] - Solargraph::Parser::NodeMethods.call_nodes_from(source_map.source.node).each do |call| + Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call| rng = Solargraph::Range.from_node(call) next if @marked_ranges.any? { |d| d.contain?(rng.start) } chain = Solargraph::Parser.chain(call, filename) @@ -646,7 +651,6 @@ def parameterized_arity_problems_for(pin, parameters, arguments, location) # @param parameters [Enumerable] # @todo need to use generic types in method to choose correct # signature and generate Integer as return type - # @sg-ignore # @return [Integer] def required_param_count(parameters) parameters.sum { |param| %i[arg kwarg].include?(param.decl) ? 1 : 0 } @@ -687,12 +691,48 @@ def fake_args_for(pin) args end + # @return [Set] + def sg_ignore_lines_processed + @sg_ignore_lines_processed ||= Set.new + end + + # @return [Set] + def all_sg_ignore_lines + source.associated_comments.select do |_line, text| + text.include?('@sg-ignore') + end.keys.to_set + end + + # @return [Array] + def unprocessed_sg_ignore_lines + (all_sg_ignore_lines - sg_ignore_lines_processed).to_a.sort + end + + # @return [Array] + def unneeded_sgignore_problems + return [] unless rules.validate_sg_ignores? + + unprocessed_sg_ignore_lines.map do |line| + Problem.new( + Location.new(filename, Range.from_to(line, 0, line, 0)), + 'Unneeded @sg-ignore comment' + ) + end + end + # @param problems [Array] # @return [Array] def without_ignored problems problems.reject do |problem| - node = source_map.source.node_at(problem.location.range.start.line, problem.location.range.start.column) - node && source_map.source.comments_for(node)&.include?('@sg-ignore') + node = source.node_at(problem.location.range.start.line, problem.location.range.start.column) + ignored = node && source.comments_for(node)&.include?('@sg-ignore') + unless !ignored || all_sg_ignore_lines.include?(problem.location.range.start.line) + # :nocov: + Solargraph.assert_or_log(:sg_ignore) { "@sg-ignore accounting issue - node is #{node}" } + # :nocov: + end + sg_ignore_lines_processed.add problem.location.range.start.line if ignored + ignored end end end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 0aad5ed8a..8f15037d5 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -57,6 +57,14 @@ def validate_tags? def require_all_return_types_match_inferred? rank >= LEVELS[:alpha] end + + # We keep this at strong because if you added an @sg-ignore to + # address a strong-level issue, then ran at a lower level, you'd + # get a false positive - we don't run stronger level checks than + # requested for performance reasons + def validate_sg_ignores? + rank >= LEVELS[:strong] + end end end end diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index ce45e5b11..0b2d84a01 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -90,7 +90,6 @@ def reporters # A hash of options supported by the formatter # - # @sg-ignore pending https://github.com/castwide/solargraph/pull/905 # @return [Hash] def formatter raw_data['formatter'] @@ -105,7 +104,6 @@ def plugins # The maximum number of files to parse from the workspace. # - # @sg-ignore pending https://github.com/castwide/solargraph/pull/905 # @return [Integer] def max_files raw_data['max_files'] diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 12db1e442..6fdf84e30 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,17 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'reports unneeded @sg-ignore tags' do + checker = type_checker(%( + class Foo + # @sg-ignore + # @return [void] + def bar; end + end + )) + expect(checker.problems.map(&:message)).to eq(['Unneeded @sg-ignore comment']) + end + it 'reports missing return tags' do checker = type_checker(%( class Foo From 8c7a5b8195d7a689b47f89ceae1e4d84098473d9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 21 Aug 2025 08:16:33 -0400 Subject: [PATCH 059/524] Document a log level env variable (#894) * Document a log level env variable * Fix logger reference * Fix env var name * 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 * 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 * 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 * Resolve params from ref tags (#872) * Resolve params from ref tags * Resolve ref tags with namespaces * Fix merge issue * RuboCop fixes * Add @sg-ignore * Linting * Linting fix * Linting fix --------- Co-authored-by: Fred Snyder --- README.md | 4 ++++ lib/solargraph/logging.rb | 14 ++++++++++++-- lib/solargraph/source/chain.rb | 6 +++++- spec/spec_helper.rb | 4 +++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3cb2ef5a..3e94a60b9 100755 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ See [https://solargraph.org/guides](https://solargraph.org/guides) for more tips ### Development +To see more logging when typechecking or running specs, set the +`SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is +the default value. + Code contributions are always appreciated. Feel free to fork the repo and submit pull requests. Check for open issues that could use help. Start new issues to discuss changes that have a major impact on the code or require large time commitments. ### Sponsorship and Donation diff --git a/lib/solargraph/logging.rb b/lib/solargraph/logging.rb index a26610fee..a8bc3b3ee 100644 --- a/lib/solargraph/logging.rb +++ b/lib/solargraph/logging.rb @@ -11,8 +11,18 @@ module Logging 'info' => Logger::INFO, 'debug' => Logger::DEBUG } - - @@logger = Logger.new(STDERR, level: DEFAULT_LOG_LEVEL) + configured_level = ENV.fetch('SOLARGRAPH_LOG', nil) + level = if LOG_LEVELS.keys.include?(configured_level) + LOG_LEVELS.fetch(configured_level) + else + if configured_level + warn "Invalid value for SOLARGRAPH_LOG: #{configured_level.inspect} - " \ + "valid values are #{LOG_LEVELS.keys}" + end + DEFAULT_LOG_LEVEL + end + # @sg-ignore Fix cvar issue + @@logger = Logger.new(STDERR, level: level) # @sg-ignore Fix cvar issue @@logger.formatter = proc do |severity, datetime, progname, msg| "[#{severity}] #{msg}\n" diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 8fdeed228..065c3bf10 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -15,6 +15,7 @@ class Source # class Chain include Equality + include Logging autoload :Link, 'solargraph/source/chain/link' autoload :Call, 'solargraph/source/chain/call' @@ -75,7 +76,9 @@ def base # Determine potential Pins returned by this chain of words # - # @param api_map [ApiMap] @param name_pin [Pin::Base] A pin + # @param api_map [ApiMap] + # + # @param name_pin [Pin::Base] A pin # representing the place in which expression is evaluated (e.g., # a Method pin, or a Module or Class pin if not run within a # method - both in terms of the closure around the chain, as well @@ -192,6 +195,7 @@ def nullable? include Logging + # @return [String] def desc links.map(&:desc).to_s end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b23c21b74..3e2631976 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,7 +27,9 @@ end require 'solargraph' # Suppress logger output in specs (if possible) -Solargraph::Logging.logger.reopen(File::NULL) if Solargraph::Logging.logger.respond_to?(:reopen) +if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') + Solargraph::Logging.logger.reopen(File::NULL) +end # @param name [String] # @param value [String] From fba485e78664078a23f2f23f5f5dbc8d4be48e56 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:44:31 -0400 Subject: [PATCH 060/524] Fix hole in type checking evaluation (#1009) * Fix hole in type checking evaluation The call to `bar()` in `[ bar('string') ].compact` is not currently type-checked due to a missed spot in `NodeMethods#call_nodes_from(node)` * Avoid over-reporting call issues * Fix annotation * Add nocov markers around unreachable area * Fix a coverage issue * Cover more paths in type checking * Fix a code coverage issue * Drop blank line * Ratchet Rubocop todo * Fix missing coverage --- .rubocop_todo.yml | 11 - .../parser/parser_gem/node_methods.rb | 1 + lib/solargraph/type_checker.rb | 223 +++++++++--------- spec/parser/node_methods_spec.rb | 40 ++++ spec/type_checker/levels/strict_spec.rb | 35 ++- spec/type_checker/levels/strong_spec.rb | 120 ++++++++++ 6 files changed, 311 insertions(+), 119 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8283c5c6..fe8ab7c48 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -99,7 +99,6 @@ Layout/ElseAlignment: - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/type_checker/rules.rb' - 'lib/solargraph/yard_map/mapper.rb' @@ -159,7 +158,6 @@ Layout/EndAlignment: - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/type_checker/rules.rb' - 'lib/solargraph/yard_map/mapper.rb' @@ -778,7 +776,6 @@ Metrics/MethodLength: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: @@ -2135,13 +2132,6 @@ Style/NegatedIfElseCondition: - 'lib/solargraph/shell.rb' - 'lib/solargraph/type_checker.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods. -# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Exclude: - - 'lib/solargraph/type_checker.rb' - # This cop supports safe autocorrection (--autocorrect). Style/NestedTernaryOperator: Exclude: @@ -2237,7 +2227,6 @@ Style/RedundantBegin: - 'lib/solargraph/shell.rb' - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/workspace.rb' # This cop supports safe autocorrection (--autocorrect). diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b716b352d..af5c62cca 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -179,6 +179,7 @@ def call_nodes_from node node.children[1..-1].each { |child| result.concat call_nodes_from(child) } elsif node.type == :send result.push node + result.concat call_nodes_from(node.children.first) node.children[2..-1].each { |child| result.concat call_nodes_from(child) } elsif [:super, :zsuper].include?(node.type) result.push node diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index e99f99195..55bf55745 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -289,127 +289,136 @@ def call_problems # @param chain [Solargraph::Source::Chain] # @param api_map [Solargraph::ApiMap] - # @param block_pin [Solargraph::Pin::Base] + # @param closure_pin [Solargraph::Pin::Closure] # @param locals [Array] # @param location [Solargraph::Location] # @return [Array] - def argument_problems_for chain, api_map, block_pin, locals, location + def argument_problems_for chain, api_map, closure_pin, locals, location result = [] base = chain - until base.links.length == 1 && base.undefined? - last_base_link = base.links.last - break unless last_base_link.is_a?(Solargraph::Source::Chain::Call) - - arguments = last_base_link.arguments - - pins = base.define(api_map, block_pin, locals) - - first_pin = pins.first - if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map) - # Do nothing, as we can't find the actual method implementation - elsif first_pin.is_a?(Pin::Method) - # @type [Pin::Method] - pin = first_pin - ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper) - arity_problems_for(pin, fake_args_for(block_pin), location) - elsif pin.path == 'Class#new' - fqns = if base.links.one? - block_pin.namespace + # @type last_base_link [Solargraph::Source::Chain::Call] + last_base_link = base.links.last + return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call) + + arguments = last_base_link.arguments + + pins = base.define(api_map, closure_pin, locals) + + first_pin = pins.first + unresolvable = first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map) + if !unresolvable && first_pin.is_a?(Pin::Method) + # @type [Pin::Method] + pin = first_pin + ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper) + arity_problems_for(pin, fake_args_for(closure_pin), location) + elsif pin.path == 'Class#new' + fqns = if base.links.one? + closure_pin.namespace + else + base.base.infer(api_map, closure_pin, locals).namespace + end + init = api_map.get_method_stack(fqns, 'initialize').first + + init ? arity_problems_for(init, arguments, location) : [] + else + arity_problems_for(pin, arguments, location) + end + return ap unless ap.empty? + return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper) + + params = first_param_hash(pins) + + all_errors = [] + pin.signatures.sort { |sig| sig.parameters.length }.each do |sig| + signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin + if signature_errors.empty? + # we found a signature that works - meaning errors from + # other signatures don't matter. + return [] + end + all_errors.concat signature_errors + end + result.concat all_errors + end + result + end + + # @param location [Location] + # @param locals [Array] + # @param closure_pin [Pin::Closure] + # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}] + # @param arguments [Array] + # @param sig [Pin::Signature] + # @param pin [Pin::Method] + # @param pins [Array] + # + # @return [Array] + def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin + errors = [] + # @todo add logic mapping up restarg parameters with + # arguments (including restarg arguments). Use tuples + # when possible, and when not, ensure provably + # incorrect situations are detected. + sig.parameters.each_with_index do |par, idx| + return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing + argchain = arguments[idx] + if argchain.nil? + if par.decl == :arg + final_arg = arguments.last + if final_arg && final_arg.node.type == :splat + argchain = final_arg + return errors else - base.base.infer(api_map, block_pin, locals).namespace + errors.push Problem.new(location, "Not enough arguments to #{pin.path}") end - init = api_map.get_method_stack(fqns, 'initialize').first - init ? arity_problems_for(init, arguments, location) : [] else - arity_problems_for(pin, arguments, location) - end - unless ap.empty? - result.concat ap - break + final_arg = arguments.last + argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type) end - break if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper) - - params = first_param_hash(pins) - - all_errors = [] - pin.signatures.sort { |sig| sig.parameters.length }.each do |sig| - errors = [] - sig.parameters.each_with_index do |par, idx| - # @todo add logic mapping up restarg parameters with - # arguments (including restarg arguments). Use tuples - # when possible, and when not, ensure provably - # incorrect situations are detected. - break if par.decl == :restarg # bail out pending better arg processing - argchain = arguments[idx] - if argchain.nil? - if par.decl == :arg - final_arg = arguments.last - if final_arg && final_arg.node.type == :splat - argchain = final_arg - next # don't try to apply the type of the splat - unlikely to be specific enough - else - errors.push Problem.new(location, "Not enough arguments to #{pin.path}") - next - end - else - final_arg = arguments.last - argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type) - end - end - if argchain - if par.decl != :arg - errors.concat kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, params, idx - next - else - if argchain.node.type == :splat && argchain == arguments.last - final_arg = argchain - end - if (final_arg && final_arg.node.type == :splat) - # The final argument given has been seen and was a - # splat, which doesn't give us useful types or - # arities against positional parameters, so let's - # continue on in case there are any required - # kwargs we should warn about - next - end - - if argchain.node.type == :splat && par != sig.parameters.last - # we have been given a splat and there are more - # arguments to come. - - # @todo Improve this so that we can skip past the - # rest of the positional parameters here but still - # process the kwargs - break - end - ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED - ptype = ptype.self_to_type(par.context) - if ptype.nil? - # @todo Some level (strong, I guess) should require the param here - else - argtype = argchain.infer(api_map, block_pin, locals) - if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) - errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") - next - end - end - end - elsif par.decl == :kwarg - errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") - next - end + end + if argchain + if par.decl != :arg + errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx + next + else + if argchain.node.type == :splat && argchain == arguments.last + final_arg = argchain + end + if (final_arg && final_arg.node.type == :splat) + # The final argument given has been seen and was a + # splat, which doesn't give us useful types or + # arities against positional parameters, so let's + # continue on in case there are any required + # kwargs we should warn about + next end - if errors.empty? - all_errors.clear - break + if argchain.node.type == :splat && par != sig.parameters.last + # we have been given a splat and there are more + # arguments to come. + + # @todo Improve this so that we can skip past the + # rest of the positional parameters here but still + # process the kwargs + return errors + end + ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED + ptype = ptype.self_to_type(par.context) + if ptype.nil? + # @todo Some level (strong, I guess) should require the param here + else + argtype = argchain.infer(api_map, closure_pin, locals) + if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) + errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") + return errors + end end - all_errors.concat errors end - result.concat all_errors + elsif par.decl == :kwarg + errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") + next end - base = base.base end - result + errors end # @param sig [Pin::Signature] diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index eb026725b..f9504b584 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -440,5 +440,45 @@ def super_with_block calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) expect(calls).to be_one end + + it 'handles chained calls' do + source = Solargraph::Source.load_string(%( + Foo.new.bar('string') + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end + + it 'handles calls from inside array literals' do + source = Solargraph::Source.load_string(%( + [ Foo.new.bar('string') ] + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end + + it 'handles calls from inside array literals that are chained' do + source = Solargraph::Source.load_string(%( + [ Foo.new.bar('string') ].compact + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(3) + end + + it 'does not over-report calls' do + source = Solargraph::Source.load_string(%( + class Foo + def something + end + end + class Bar < Foo + def something + super(1) + 2 + end + end + )) + calls = Solargraph::Parser::NodeMethods.call_nodes_from(source.node) + expect(calls.length).to eq(2) + end end end diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index b198cec89..25890683b 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -115,6 +115,39 @@ def bar(baz); end expect(checker.problems.first.message).to include('Wrong argument type') end + it 'reports mismatched argument types in chained calls' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + bar('string').upcase + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + + it 'reports mismatched argument types in calls inside array literals' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + [ bar('string') ] + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + + it 'reports mismatched argument types in calls inside array literals used in a chain' do + checker = type_checker(%( + # @param baz [Integer] + # @return [String] + def bar(baz); "foo"; end + [ bar('string') ].compact + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('Wrong argument type') + end + xit 'complains about calling a private method from an illegal place' xit 'complains about calling a non-existent method' @@ -126,7 +159,7 @@ def foo(a) a[0] = :something end )) - expect(checker.problems.map(&:problems)).to eq(['Wrong argument type']) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type']) end it 'complains about dereferencing a non-existent tuple slot' diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 6fdf84e30..a03e6eb5d 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,48 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'does not complain on array dereference' do + checker = type_checker(%( + # @param idx [Integer, nil] an index + # @param arr [Array] an array of integers + # + # @return [void] + def foo(idx, arr) + arr[idx] + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'complains on bad @type assignment' do + checker = type_checker(%( + # @type [Integer] + c = Class.new + )) + expect(checker.problems.map(&:message)) + .to eq ['Declared type Integer does not match inferred type Class for variable c'] + end + + it 'does not complain on another variant of Class.new' do + checker = type_checker(%( + class Class + # @return [self] + def self.blah + new + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on indirect Class.new', skip: 'hangs in a loop currently' do + checker = type_checker(%( + class Foo < Class; end + Foo.new + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'reports unneeded @sg-ignore tags' do checker = type_checker(%( class Foo @@ -25,6 +67,84 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end + it 'ignores nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'calls out keyword issues even when required arg count matches' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo('baz') + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + + it 'calls out type issues even when keyword issues are there' do + pending('fixes to arg vs param checking algorithm') + + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)) + .to include('Wrong argument type for #foo: a expected String, received 123') + end + + it 'calls out keyword issues even when arg type issues are there' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b:); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') + end + + it 'calls out missing args after a defaulted param' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)).to include('Not enough arguments to #foo') + end + it 'reports missing param tags' do checker = type_checker(%( class Foo From f61b1b6a94439c21b0eb0a986053b5d88276316f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:47:12 -0400 Subject: [PATCH 061/524] Improve typechecking error message (#1014) If we know the target of an unresolved method call, include it in the error message. --- lib/solargraph/type_checker.rb | 6 +++++- spec/type_checker/levels/strict_spec.rb | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 55bf55745..e0b23f56b 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -277,7 +277,11 @@ def call_problems # @todo remove the internal_or_core? check at a higher-than-strict level if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) unless closest.generic? || ignored_pins.include?(found) - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}") + if closest.defined? + result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") + else + result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}") + end @marked_ranges.push rng end end diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 25890683b..0e2159d95 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -40,6 +40,7 @@ def bar(a); end )) expect(checker.problems).to be_one expect(checker.problems.first.message).to include('Unresolved call') + expect(checker.problems.first.message).not_to include('undefined') end it 'reports undefined method calls with defined roots' do @@ -48,6 +49,7 @@ def bar(a); end )) expect(checker.problems).to be_one expect(checker.problems.first.message).to include('Unresolved call') + expect(checker.problems.first.message).to include('String') expect(checker.problems.first.message).to include('not_a_method') end From 9d4c711bed1af477224bae346b98e93a2d2e732e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:48:37 -0400 Subject: [PATCH 062/524] Internal strict type-checking fixes (#1013) * Internal strict type-checking fixes * Add annotation * Add annotation * Add @sg-ignores for now --- lib/solargraph/api_map/cache.rb | 5 +++-- lib/solargraph/api_map/index.rb | 8 ++++---- lib/solargraph/api_map/store.rb | 1 + lib/solargraph/complex_type/unique_type.rb | 4 ++-- lib/solargraph/diagnostics/rubocop_helpers.rb | 1 - lib/solargraph/library.rb | 3 ++- lib/solargraph/parser/node_processor/base.rb | 2 +- .../parser/parser_gem/node_processors/block_node.rb | 5 +++-- .../parser/parser_gem/node_processors/if_node.rb | 2 ++ lib/solargraph/rbs_map/conversions.rb | 2 +- lib/solargraph/source.rb | 2 +- lib/solargraph/source_map/clip.rb | 2 +- lib/solargraph/yard_map/mapper/to_method.rb | 2 +- 13 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/solargraph/api_map/cache.rb b/lib/solargraph/api_map/cache.rb index 329a1e5e1..0052d91ea 100644 --- a/lib/solargraph/api_map/cache.rb +++ b/lib/solargraph/api_map/cache.rb @@ -4,9 +4,9 @@ module Solargraph class ApiMap class Cache def initialize - # @type [Hash{Array => Array}] + # @type [Hash{String => Array}] @methods = {} - # @type [Hash{(String, Array) => Array}] + # @type [Hash{String, Array => Array}] @constants = {} # @type [Hash{String => String}] @qualified_namespaces = {} @@ -101,6 +101,7 @@ def empty? private + # @return [Array] def all_caches [@methods, @constants, @qualified_namespaces, @receiver_definitions, @clips] end diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index ea358297e..5237a3802 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -42,22 +42,22 @@ def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references @include_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references @extend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references @prepend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index d41a2a0ae..87f053596 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -210,6 +210,7 @@ def index # @return [Boolean] def catalog pinsets @pinsets = pinsets + # @type [Array] @indexes = [] pinsets.each do |pins| if @indexes.last && pins.empty? diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 63a6ae15b..a782656f0 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -349,9 +349,9 @@ def to_a # @param new_name [String, nil] # @param make_rooted [Boolean, nil] - # @param new_key_types [Array, nil] + # @param new_key_types [Array, nil] # @param rooted [Boolean, nil] - # @param new_subtypes [Array, nil] + # @param new_subtypes [Array, nil] # @return [self] def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil) raise "Please remove leading :: and set rooted instead - #{new_name}" if new_name&.start_with?('::') diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index 4eb2c711d..bfae43822 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -19,7 +19,6 @@ def require_rubocop(version = nil) gem_path = Gem::Specification.find_by_name('rubocop', version).full_gem_path gem_lib_path = File.join(gem_path, 'lib') $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path) - # @todo Gem::MissingSpecVersionError is undocumented for some reason # @sg-ignore rescue Gem::MissingSpecVersionError => e raise InvalidRubocopVersionError, diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 72224f672..b4da03b2e 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -500,7 +500,7 @@ def external_requires private - # @return [Hash{String => Set}] + # @return [Hash{String => Array}] def source_map_external_require_hash @source_map_external_require_hash ||= {} end @@ -508,6 +508,7 @@ def source_map_external_require_hash # @param source_map [SourceMap] # @return [void] def find_external_requires source_map + # @type [Set] new_set = source_map.requires.map(&:name).to_set # return if new_set == source_map_external_require_hash[source_map.filename] _filenames = nil diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index d87268a91..fad31e95b 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -13,7 +13,7 @@ class Base # @return [Array] attr_reader :pins - # @return [Array] + # @return [Array] attr_reader :locals # @param node [Parser::AST::Node] diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index 70a2d9e68..d773e8e50 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -19,7 +19,7 @@ def process else region.closure end - pins.push Solargraph::Pin::Block.new( + block_pin = Solargraph::Pin::Block.new( location: location, closure: parent, node: node, @@ -28,7 +28,8 @@ def process scope: region.scope || region.closure.context.scope, source: :parser ) - process_children region.update(closure: pins.last) + pins.push block_pin + process_children region.update(closure: block_pin) end private diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index 5784afcbe..2452b9cc5 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -11,6 +11,8 @@ def process process_children position = get_node_start_position(node) + # @sg-ignore + # @type [Solargraph::Pin::Breakable, nil] enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 6e50c022a..f8deec251 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -345,7 +345,7 @@ def global_decl_to_pin decl } # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrAccessor] - # @param closure [Pin::Namespace] + # @param closure [Pin::Closure] # @param context [Context] # @param scope [Symbol] :instance or :class # @param name [String] The name of the method diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index d2b24cc61..ee8baa768 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -318,7 +318,7 @@ def string_nodes @string_nodes ||= string_nodes_in(node) end - # @return [Array] + # @return [Array] def comment_ranges @comment_ranges ||= comments.values.map(&:range) end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index ba69b1b93..16a4ec845 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -65,7 +65,7 @@ def infer # position. Locals can be local variables, method parameters, or block # parameters. The array starts with the nearest local pin. # - # @return [::Array] + # @return [::Array] def locals @locals ||= source_map.locals_at(location) end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index df431bb3c..6bb4fa261 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -27,7 +27,7 @@ def self.make code_object, name = nil, scope = nil, visibility = nil, closure = final_scope = scope || code_object.scope override_key = [closure.path, final_scope, name] final_visibility = VISIBILITY_OVERRIDE[override_key] - final_visibility ||= VISIBILITY_OVERRIDE[override_key[0..-2]] + final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]] final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name) final_visibility ||= visibility final_visibility ||= :private if code_object.module_function? && final_scope == :instance From 3bcbf85d5a5e7445754b1a0392e39238f8a681c3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:50:13 -0400 Subject: [PATCH 063/524] Reproduce and fix a ||= (or-asgn) evaluation issue (#1017) * Reproduce and fix a ||= (or-asgn) evaluation issue * Fix linting issue --- .../parser/parser_gem/node_chainer.rb | 3 ++- spec/type_checker/levels/strict_spec.rb | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index 646e753d5..d8d46319b 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -99,7 +99,8 @@ def generate_links n elsif [:gvar, :gvasgn].include?(n.type) result.push Chain::GlobalVariable.new(n.children[0].to_s) elsif n.type == :or_asgn - result.concat generate_links n.children[1] + new_node = n.updated(n.children[0].type, n.children[0].children + [n.children[1]]) + result.concat generate_links new_node elsif [:class, :module, :def, :defs].include?(n.type) # @todo Undefined or what? result.push Chain::UNDEFINED_CALL diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 0e2159d95..7e57cb7cf 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -976,5 +976,22 @@ def bar(a) )) expect(checker.problems.map(&:message)).to eq([]) end + + it 'does not complain on defaulted reader with detailed expression' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + if rand + 123 + elsif rand + 456 + end + end + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end end end From 25557b42fea981ddf8d5a01042204e55c71fdab5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:51:19 -0400 Subject: [PATCH 064/524] Define closure for Pin::Symbol, for completeness (#1027) This isn't used anywhere to my knowledge, but it makes sense to think of symbols as being in the global namespace, helps guarantee that closure is always available on a pin, and of course keeps the assert happy ;) --- lib/solargraph/pin/symbol.rb | 4 ++++ spec/pin/symbol_spec.rb | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/symbol.rb b/lib/solargraph/pin/symbol.rb index 9e11c3d7d..9c59155a1 100644 --- a/lib/solargraph/pin/symbol.rb +++ b/lib/solargraph/pin/symbol.rb @@ -20,6 +20,10 @@ def path '' end + def closure + @closure ||= Pin::ROOT_PIN + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::KEYWORD end diff --git a/spec/pin/symbol_spec.rb b/spec/pin/symbol_spec.rb index 98d88137e..16961cadc 100644 --- a/spec/pin/symbol_spec.rb +++ b/spec/pin/symbol_spec.rb @@ -1,10 +1,15 @@ describe Solargraph::Pin::Symbol do context "as an unquoted literal" do - it "is a kind of keyword" do + it "is a kind of keyword to the LSP" do pin = Solargraph::Pin::Symbol.new(nil, ':symbol') expect(pin.completion_item_kind).to eq(Solargraph::LanguageServer::CompletionItemKinds::KEYWORD) end + it "has global closure" do + pin = Solargraph::Pin::Symbol.new(nil, ':symbol') + expect(pin.closure).to eq(Solargraph::Pin::ROOT_PIN) + end + it "has a Symbol return type" do pin = Solargraph::Pin::Symbol.new(nil, ':symbol') expect(pin.return_type.tag).to eq('Symbol') From 32565d4488765c6a4ebfc302ee24bb123dd5c5af Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 10:52:07 -0400 Subject: [PATCH 065/524] Fix 'all!' config to reporters (#1018) * Fix 'all!' config to reporters Solargraph found the type error here! * Linting fixes --- lib/solargraph/library.rb | 4 ++-- spec/library_spec.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index b4da03b2e..9d5162431 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -402,8 +402,8 @@ def diagnose filename repargs = {} workspace.config.reporters.each do |line| if line == 'all!' - Diagnostics.reporters.each do |reporter| - repargs[reporter] ||= [] + Diagnostics.reporters.each do |reporter_name| + repargs[Diagnostics.reporter(reporter_name)] ||= [] end else args = line.split(':').map(&:strip) diff --git a/spec/library_spec.rb b/spec/library_spec.rb index bea0f2983..34de9e1f0 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -132,6 +132,20 @@ def bar baz, key: '' # @todo More tests end + it 'diagnoses using all reporters' do + directory = '' + config = instance_double(Solargraph::Workspace::Config) + allow(config).to receive_messages(plugins: [], required: [], reporters: ['all!']) + workspace = Solargraph::Workspace.new directory, config + library = Solargraph::Library.new workspace + src = Solargraph::Source.load_string(%( + puts 'hello' + ), 'file.rb', 0) + library.attach src + result = library.diagnose 'file.rb' + expect(result.to_s).to include('rubocop') + end + it "documents symbols" do library = Solargraph::Library.new src = Solargraph::Source.load_string(%( From 3946cc481c12c704bd9265b87516079d179ce5a2 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Sun, 24 Aug 2025 11:54:48 -0400 Subject: [PATCH 066/524] Fix DocMap.all_rbs_collection_gems_in_memory return type (#1037) --- 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 43c8768b0..56f51973f 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -133,7 +133,7 @@ def self.all_yard_gems_in_memory @yard_gems_in_memory ||= {} end - # @return [Hash{String => Array}] stored by RBS collection path + # @return [Hash{String => Hash{Array(String, String) => Array}}] stored by RBS collection path def self.all_rbs_collection_gems_in_memory @rbs_collection_gems_in_memory ||= {} end From 43caccadaaf71bbd51acc85d74d437fd061875f1 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Sun, 24 Aug 2025 12:40:56 -0400 Subject: [PATCH 067/524] Fix RuboCop linting errors in regular expressions (#1038) * Fix RuboCop linting errors in regular expressions * Continue on rubocop_todo errors * Move configuration * Continue on undercover errors --- .github/workflows/linting.yml | 1 + .github/workflows/rspec.yml | 1 + lib/solargraph/parser/parser_gem/node_methods.rb | 2 +- lib/solargraph/pin/method.rb | 4 ++-- lib/solargraph/pin/parameter.rb | 2 +- lib/solargraph/source/change.rb | 4 ++-- lib/solargraph/source/cursor.rb | 4 ++-- lib/solargraph/source/source_chainer.rb | 2 +- lib/solargraph/source_map/mapper.rb | 4 ++-- 9 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 2a5968351..8abbf51ef 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -113,6 +113,7 @@ jobs: run: bundle exec rubocop -c .rubocop.yml - name: Run RuboCop against todo file + continue-on-error: true run: | bundle exec rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp if [ -n "$(git status --porcelain)" ] diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 35f7a1d13..ecc3d9771 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -69,3 +69,4 @@ jobs: run: bundle exec rake spec - name: Check PR coverage run: bundle exec rake undercover + continue-on-error: true diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index af5c62cca..1397f9583 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -243,7 +243,7 @@ def find_recipient_node cursor if source.synchronized? return node if source.code[0..offset-1] =~ /\(\s*\z/ && source.code[offset..-1] =~ /^\s*\)/ else - return node if source.code[0..offset-1] =~ /\([^\(]*\z/ + return node if source.code[0..offset-1] =~ /\([^(]*\z/ end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 6309cb55a..0482b2b54 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -428,7 +428,7 @@ def resolve_ref_tag api_map @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?(/[#\.]/) + ref = if tag.owner.to_s.start_with?(/[#.]/) api_map.get_methods(namespace) .select { |pin| pin.path.end_with?(tag.owner.to_s) } .first @@ -552,7 +552,7 @@ def typify_from_super api_map # @param api_map [ApiMap] # @return [ComplexType, nil] def resolve_reference ref, api_map - parts = ref.split(/[\.#]/) + parts = ref.split(/[.#]/) if parts.first.empty? || parts.one? path = "#{namespace}#{ref}" else diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index e298ba20a..512ee0ead 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -247,7 +247,7 @@ def see_reference heredoc, api_map, skip = [] def resolve_reference ref, api_map, skip return nil if skip.include?(ref) skip.push ref - parts = ref.split(/[\.#]/) + parts = ref.split(/[.#]/) if parts.first.empty? path = "#{namespace}#{ref}" else diff --git a/lib/solargraph/source/change.rb b/lib/solargraph/source/change.rb index 72a99b6a6..65c47c7e0 100644 --- a/lib/solargraph/source/change.rb +++ b/lib/solargraph/source/change.rb @@ -28,7 +28,7 @@ def initialize range, new_text # syntax errors will be repaired. # @return [String] The updated text. def write text, nullable = false - if nullable and !range.nil? and new_text.match(/[\.\[\{\(@\$:]$/) + if nullable and !range.nil? and new_text.match(/[.\[{(@$:]$/) [':', '@'].each do |dupable| next unless new_text == dupable offset = Position.to_offset(text, range.start) @@ -59,7 +59,7 @@ def repair text else result = commit text, fixed off = Position.to_offset(text, range.start) - match = result[0, off].match(/[\.:]+\z/) + match = result[0, off].match(/[.:]+\z/) if match result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..-1] end diff --git a/lib/solargraph/source/cursor.rb b/lib/solargraph/source/cursor.rb index 70e4fd47a..a8226eb07 100644 --- a/lib/solargraph/source/cursor.rb +++ b/lib/solargraph/source/cursor.rb @@ -124,7 +124,7 @@ def node def node_position @node_position ||= begin if start_of_word.empty? - match = source.code[0, offset].match(/[\s]*(\.|:+)[\s]*$/) + match = source.code[0, offset].match(/\s*(\.|:+)\s*$/) if match Position.from_offset(source.code, offset - match[0].length) else @@ -159,7 +159,7 @@ def start_word_pattern # # @return [Regexp] def end_word_pattern - /^([a-z0-9_]|[^\u0000-\u007F])*[\?\!]?/i + /^([a-z0-9_]|[^\u0000-\u007F])*[?!]?/i end end end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index e79d85b7e..5758a9d35 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -97,7 +97,7 @@ def fixed_position # @return [String] def end_of_phrase @end_of_phrase ||= begin - match = phrase.match(/[\s]*(\.{1}|::)[\s]*$/) + match = phrase.match(/\s*(\.{1}|::)\s*$/) if match match[0] else diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index d53fd49a0..5fdcb9fe6 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -163,7 +163,7 @@ def process_directive source_position, comment_position, directive end end when 'visibility' - begin + kind = directive.tag.text&.to_sym return unless [:private, :protected, :public].include?(kind) @@ -182,7 +182,7 @@ def process_directive source_position, comment_position, directive pin.instance_variable_set(:@visibility, kind) end end - end + when 'parse' begin ns = closure_at(source_position) From d3bdfea12869252296c8b2cf9ca1ce2186f86321 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 24 Aug 2025 13:33:58 -0400 Subject: [PATCH 068/524] Resolve class aliases via Constant pins (#1029) * Resolve class aliases via Constant pins This also eliminates the need for Parser::NodeMethods as a searately defined class. * Resolve merge issues * Resolve Solargraph strong complaint * Add @sg-ignore * Fix RuboCop issues * Drop unused method * Ratchet .rubocop_todo.yml --- .rubocop_todo.yml | 11 +- lib/solargraph.rb | 18 +- lib/solargraph/api_map.rb | 118 +++++++++++--- lib/solargraph/api_map/store.rb | 9 +- lib/solargraph/complex_type.rb | 3 + lib/solargraph/complex_type/type_methods.rb | 12 +- lib/solargraph/complex_type/unique_type.rb | 5 +- lib/solargraph/parser/node_methods.rb | 97 ----------- .../parser/parser_gem/node_methods.rb | 2 +- lib/solargraph/pin/base.rb | 2 +- lib/solargraph/rbs_map/conversions.rb | 2 +- spec/api_map/api_map_method_spec.rb | 154 ++++++++++++++++++ spec/api_map_spec.rb | 59 +++++++ spec/convention/struct_definition_spec.rb | 4 +- 14 files changed, 355 insertions(+), 141 deletions(-) delete mode 100644 lib/solargraph/parser/node_methods.rb create mode 100644 spec/api_map/api_map_method_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fe8ab7c48..14cc0ca5d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -397,7 +397,6 @@ Layout/SpaceBeforeComma: # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/diagnostics/update_errors.rb' - 'lib/solargraph/language_server/host.rb' @@ -569,7 +568,6 @@ Lint/NonAtomicFileOperation: # This cop supports safe autocorrection (--autocorrect). Lint/ParenthesesAsGroupedExpression: Exclude: - - 'lib/solargraph.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'spec/language_server/host_spec.rb' - 'spec/source_map/clip_spec.rb' @@ -672,7 +670,6 @@ Lint/UselessAccessModifier: # Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - 'lib/solargraph/language_server/message/extended/document_gems.rb' @@ -757,6 +754,7 @@ Metrics/ClassLength: # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: Exclude: + - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' @@ -1057,7 +1055,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1098,7 +1095,6 @@ RSpec/ImplicitExpect: RSpec/InstanceVariable: Exclude: - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host_spec.rb' @@ -1282,6 +1278,7 @@ RSpec/ScatteredLet: RSpec/SpecFilePathFormat: Exclude: - '**/spec/routing/**/*' + - 'spec/api_map/api_map_method_spec.rb' - 'spec/api_map/cache_spec.rb' - 'spec/api_map/config_spec.rb' - 'spec/api_map/source_to_yard_spec.rb' @@ -1622,7 +1619,6 @@ Style/Documentation: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' @@ -1763,7 +1759,6 @@ Style/FrozenStringLiteralComment: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/snippet.rb' - 'lib/solargraph/pin/breakable.rb' @@ -2037,7 +2032,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/location.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem/flawed_builder.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' @@ -2249,7 +2243,6 @@ Style/RedundantInitialize: # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: - - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 038e7bccf..8520e3a93 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -72,7 +72,23 @@ def self.asserts_on?(type) # @param block [Proc] A block that returns a message to log # @return [void] def self.assert_or_log(type, msg = nil, &block) - raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type) + if asserts_on? type + # @type [String, nil] + msg ||= block.call + + raise "No message given for #{type.inspect}" if msg.nil? + + # @todo :combine_with_visibility is not ready for prime time - + # lots of disagreements found in practice that heuristics need + # to be created for and/or debugging needs to resolve in pin + # generation. + # @todo :api_map_namespace_pin_stack triggers in a badly handled + # self type case - 'keeps track of self type in method + # parameters in subclass' in call_spec.rb + return if %i[api_map_namespace_pin_stack combine_with_visibility].include?(type) + + raise msg + end logger.info msg, &block end diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index eed02b4ef..9db21128f 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -308,12 +308,11 @@ def qualify tag, context_tag = '' return unless type return tag if type.literal? - context_type = ComplexType.try_parse(context_tag) - return unless context_type - fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace) return unless fqns + return fqns if %w[Class Module].include? type + fqns + type.substring end @@ -406,16 +405,18 @@ def get_block_pins # @param deep [Boolean] True to include superclasses, mixins, etc. # @return [Array] def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true + rooted_tag = qualify(rooted_tag, '') + return [] unless rooted_tag if rooted_tag.start_with? 'Array(' # Array() are really tuples - use our fill, as the RBS repo # does not give us definitions for it rooted_tag = "Solargraph::Fills::Tuple(#{rooted_tag[6..-2]})" end - rooted_type = ComplexType.try_parse(rooted_tag) - fqns = rooted_type.namespace - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first cached = cache.get_methods(rooted_tag, scope, visibility, deep) return cached.clone unless cached.nil? + rooted_type = ComplexType.try_parse(rooted_tag) + fqns = rooted_type.namespace + namespace_pin = get_namespace_pin(fqns) # @type [Array] result = [] skip = Set.new @@ -535,10 +536,20 @@ def get_complex_type_methods complex_type, context = '', internal = false # @param visibility [Array] :public, :protected, and/or :private # @param preserve_generics [Boolean] # @return [Array] - def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false - rooted_type = ComplexType.parse(rooted_tag) + def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], + preserve_generics: false + rooted_tag = qualify(rooted_tag, '') + return [] unless rooted_tag + rooted_type = ComplexType.try_parse(rooted_tag) + return [] if rooted_type.nil? fqns = rooted_type.namespace - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + namespace_pin = get_namespace_pin(fqns) + if namespace_pin.nil? + # :nocov: + Solargraph.assert_or_log(:api_map_namespace_pin_stack, "Could not find namespace pin for #{fqns} while looking for method #{name}") + return [] + # :nocov: + end methods = get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name } methods = erase_generics(namespace_pin, rooted_type, methods) unless preserve_generics methods @@ -695,7 +706,7 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] # @param skip [Set] # @param no_core [Boolean] Skip core classes if true # @return [Array] - def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core) + def inner_get_methods_from_reference fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" } # Ensure the types returned by the methods in the referenced @@ -709,7 +720,7 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop # @todo Can inner_get_methods be cached? Lots of lookups of base types going on. methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core) if namespace_pin && !resolved_reference_type.all_params.empty? - reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first + reference_pin = get_namespace_pin(resolved_reference_type.namespace) # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" } methods = methods.map do |method_pin| method_pin.resolve_generics(reference_pin, resolved_reference_type) @@ -734,6 +745,13 @@ def store # @return [Solargraph::ApiMap::Cache] attr_reader :cache + # @param fqns [String] + # @return [Pin::Namespace, nil] + def get_namespace_pin fqns + # fqns = ComplexType.parse(fqns).namespace + store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + end + # @param rooted_tag [String] A fully qualified namespace, with # generic parameter values if applicable # @param scope [Symbol] :class or :instance @@ -743,11 +761,20 @@ def store # @param no_core [Boolean] Skip core classes if true # @return [Array] def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false + rooted_tag = qualify(rooted_tag, '') + return [] if rooted_tag.nil? + return [] unless rooted_tag rooted_type = ComplexType.parse(rooted_tag).force_rooted fqns = rooted_type.namespace - fqns_generic_params = rooted_type.all_params - namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first + namespace_pin = get_namespace_pin(fqns) + if namespace_pin.nil? + # :nocov: + Solargraph.assert_or_log(:api_map_namespace_pin_inner, "Could not find namespace pin for #{fqns}") + return [] + # :nocov: + end return [] if no_core && fqns =~ /^(Object|BasicObject|Class|Module)$/ + # @todo should this by by rooted_tag_? reqstr = "#{fqns}|#{scope}|#{visibility.sort}|#{deep}" return [] if skip.include?(reqstr) skip.add reqstr @@ -770,7 +797,10 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |include_tag| rooted_include_tag = qualify(include_tag, rooted_tag) - result.concat inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) + if rooted_include_tag + result.concat inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, + visibility, deep, skip, true) + end end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? @@ -864,16 +894,21 @@ def inner_qualify name, root, skip if root == '' return '' else + root = root[2..-1] if root&.start_with?('::') return inner_qualify(root, '', skip) end else - return name if root == '' && store.namespace_exists?(name) roots = root.to_s.split('::') while roots.length > 0 - fqns = roots.join('::') + '::' + name - return fqns if store.namespace_exists?(fqns) - incs = store.get_includes(roots.join('::')) + potential_root = roots.join('::') + potential_root = potential_root[2..-1] if potential_root.start_with?('::') + potential_fqns = potential_root + '::' + name + potential_fqns = potential_fqns[2..-1] if potential_fqns.start_with?('::') + fqns = resolve_fqns(potential_fqns) + return fqns if fqns + incs = store.get_includes(potential_root) incs.each do |inc| + next if potential_root == root && inc == name foundinc = inner_qualify(name, inc, skip) possibles.push foundinc unless foundinc.nil? end @@ -886,11 +921,54 @@ def inner_qualify name, root, skip possibles.push foundinc unless foundinc.nil? end end - return name if store.namespace_exists?(name) + resolved_fqns = resolve_fqns(name) + return resolved_fqns if resolved_fqns + return possibles.last end end + # @param fqns [String] + # @return [String, nil] + def resolve_fqns fqns + return fqns if store.namespace_exists?(fqns) + + constant_namespace = nil + constant = store.constant_pins.find do |c| + constant_fqns = if c.namespace.empty? + c.name + else + c.namespace + '::' + c.name + end + constant_namespace = c.namespace + constant_fqns == fqns + end + return nil unless constant + + return constant.return_type.namespace if constant.return_type.defined? + + assignment = constant.assignment + + # @sg-ignore Wrong argument type for Solargraph::ApiMap#resolve_trivial_constant: node expected AST::Node, received Parser::AST::Node, nil + target_ns = resolve_trivial_constant(assignment) if assignment + return nil unless target_ns + qualify_namespace target_ns, constant_namespace + end + + # @param node [AST::Node] + # @return [String, nil] + def resolve_trivial_constant node + return nil unless node.is_a?(::Parser::AST::Node) + return nil unless node.type == :const + return nil if node.children.empty? + prefix_node = node.children[0] + prefix = '' + prefix = resolve_trivial_constant(prefix_node) + '::' unless prefix_node.nil? || prefix_node.children.empty? + const_name = node.children[1].to_s + return nil if const_name.empty? + return prefix + const_name + end + # Get the namespace's type (Class or Module). # # @param fqns [String] A fully qualified namespace @@ -898,7 +976,7 @@ def inner_qualify name, root, skip def get_namespace_type fqns return nil if fqns.nil? # @type [Pin::Namespace, nil] - pin = store.get_path_pins(fqns).select{|p| p.is_a?(Pin::Namespace)}.first + pin = get_namespace_pin(fqns) return nil if pin.nil? pin.type end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 87f053596..3b3fffd69 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -73,13 +73,13 @@ def get_methods fqns, scope: :instance, visibility: [:public] def get_superclass fq_tag raise "Do not prefix fully qualified tags with '::' - #{fq_tag.inspect}" if fq_tag.start_with?('::') sub = ComplexType.parse(fq_tag) + return sub.simplify_literals.name if sub.literal? + return 'Boolean' if %w[TrueClass FalseClass].include?(fq_tag) fqns = sub.namespace return superclass_references[fq_tag].first if superclass_references.key?(fq_tag) 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 @@ -143,6 +143,11 @@ def namespace_pins pins_by_class(Solargraph::Pin::Namespace) end + # @return [Enumerable] + def constant_pins + pins_by_class(Solargraph::Pin::Constant) + end + # @return [Enumerable] def method_pins pins_by_class(Solargraph::Pin::Method) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index ac9599329..00dda2d3e 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -17,10 +17,13 @@ def initialize types = [UniqueType::UNDEFINED] # @todo @items here should not need an annotation # @type [Array] items = types.flat_map(&:items).uniq(&:to_s) + + # Canonicalize 'true, false' to the non-runtime-type 'Boolean' if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' } items.delete_if { |i| i.name == 'false' || i.name == 'true' } items.unshift(ComplexType::BOOLEAN) end + items = [UniqueType::UNDEFINED] if items.any?(&:undefined?) @items = items end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index e6d596244..6bf383a1a 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -10,11 +10,7 @@ class ComplexType # @name: String # @subtypes: Array # @rooted: boolish - # methods: - # transform() - # all_params() - # rooted?() - # can_root_name?() + # methods: (see @!method declarations below) module TypeMethods # @!method transform(new_name = nil, &transform_type) # @param new_name [String, nil] @@ -24,6 +20,9 @@ module TypeMethods # @!method all_params # @return [Array] # @!method rooted? + # @!method literal? + # @!method simplify_literals + # @return [ComplexType::UniqueType, ComplexType] # @!method can_root_name?(name_to_check = nil) # @param name_to_check [String, nil] @@ -124,7 +123,8 @@ def key_types def namespace # if priority higher than ||=, old implements cause unnecessary check @namespace ||= lambda do - return 'Object' if duck_type? + return simplify_literals.namespace if literal? + return 'Object' if duck_type? || name == 'Boolean' return 'NilClass' if nil_type? return (name == 'Class' || name == 'Module') && !subtypes.empty? ? subtypes.first.name : name end.call diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index a782656f0..86a69fe0f 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -74,6 +74,8 @@ def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: ni if parameters_type.nil? raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty? end + + raise "name must be a String" unless name.is_a?(String) raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::') @name = name @parameters_type = parameters_type @@ -126,7 +128,8 @@ def determine_non_literal_name # | `false` return name if name.empty? return 'NilClass' if name == 'nil' - return 'Boolean' if ['true', 'false'].include?(name) + return 'TrueClass' if name == 'true' + return 'FalseClass' if name == 'false' return 'Symbol' if name[0] == ':' return 'String' if ['"', "'"].include?(name[0]) return 'Integer' if name.match?(/^-?\d+$/) diff --git a/lib/solargraph/parser/node_methods.rb b/lib/solargraph/parser/node_methods.rb deleted file mode 100644 index 5d3d1079a..000000000 --- a/lib/solargraph/parser/node_methods.rb +++ /dev/null @@ -1,97 +0,0 @@ -module Solargraph - module Parser - module NodeMethods - module_function - - # @abstract - # @param node [Parser::AST::Node] - # @return [String] - def unpack_name node - raise NotImplementedError - end - - # @abstract - # @todo Temporarily here for testing. Move to Solargraph::Parser. - # @param node [Parser::AST::Node] - # @return [Array] - def call_nodes_from node - raise NotImplementedError - end - - # Find all the nodes within the provided node that potentially return a - # value. - # - # The node parameter typically represents a method's logic, e.g., the - # second child (after the :args node) of a :def node. A simple one-line - # method would typically return itself, while a node with conditions - # would return the resulting node from each conditional branch. Nodes - # that follow a :return node are assumed to be unreachable. Nil values - # are converted to nil node types. - # - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] - def returns_from_method_body node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # - # @return [Array] - def const_nodes_from node - raise NotImplementedError - end - - # @abstract - # @param cursor [Solargraph::Source::Cursor] - # @return [Parser::AST::Node, nil] - def find_recipient_node cursor - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] low-level value nodes in - # value position. Does not include explicit return - # statements - def value_position_nodes_only(node) - raise NotImplementedError - end - - # @abstract - # @param nodes [Enumerable] - def any_splatted_call?(nodes) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [void] - def process node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Source::Chain}] - def convert_hash node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_start_position(node) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_end_position(node) - raise NotImplementedError - end - end - end -end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 1397f9583..5b1c47996 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -120,7 +120,7 @@ def drill_signature node, signature end # @param node [Parser::AST::Node] - # @return [Hash{Parser::AST::Node => Chain}] + # @return [Hash{Parser::AST::Node, Symbol => Source::Chain}] def convert_hash node return {} unless Parser.is_ast_node?(node) return convert_hash(node.children[0]) if node.type == :kwsplat diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index fb3274dab..020d92def 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -300,8 +300,8 @@ def assert_same_count(other, attr) # # @return [Object, nil] def assert_same(other, attr) - return false if other.nil? val1 = send(attr) + return val1 if other.nil? val2 = other.send(attr) return val1 if val1 == val2 Solargraph.assert_or_log("combine_with_#{attr}".to_sym, diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index f8deec251..657ea982f 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -240,6 +240,7 @@ def module_decl_to_pin decl # # @return [Solargraph::Pin::Constant] def create_constant(name, tag, comments, decl, base = nil) + tag = "#{base}<#{tag}>" if base parts = name.split('::') if parts.length > 1 name = parts.last @@ -255,7 +256,6 @@ def create_constant(name, tag, comments, decl, base = nil) comments: comments, source: :rbs ) - tag = "#{base}<#{tag}>" if base rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) constant_pin diff --git a/spec/api_map/api_map_method_spec.rb b/spec/api_map/api_map_method_spec.rb new file mode 100644 index 000000000..a3adc9b94 --- /dev/null +++ b/spec/api_map/api_map_method_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +describe Solargraph::ApiMap do + let(:api_map) { described_class.new } + let(:bench) do + Solargraph::Bench.new(external_requires: external_requires, workspace: Solargraph::Workspace.new('.')) + end + let(:external_requires) { [] } + + before do + api_map.catalog bench + end + + describe '#qualify' do + let(:external_requires) { ['yaml'] } + + it 'resolves YAML to Psych' do + expect(api_map.qualify('YAML', '')).to eq('Psych') + end + + it 'resolves constants used to alias namespaces' do + map = Solargraph::SourceMap.load_string(%( + class Foo + def bing; end + end + + module Bar + Baz = ::Foo + end + )) + api_map.index map.pins + fqns = api_map.qualify('Bar::Baz') + expect(fqns).to eq('Foo') + end + + it 'understands alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + class Foo + # @return [Symbol] + def bing; end + end + + module Bar + Baz = ::Foo + end + + a = Bar::Baz.new.bing + a + Bar::Baz + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [11, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands nested alias namespaces to nested classes resolving types' do + source = Solargraph::Source.load_string(%( + module A + class Foo + # @return [Symbol] + def bing; end + end + end + + module Bar + Baz = A::Foo + end + + a = Bar::Baz.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [13, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands nested alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + module Bar + module A + class Foo + # @return [Symbol] + def bing; :bingo; end + end + end + end + + module Bar + Foo = A::Foo + end + + a = Bar::Foo.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [15, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + + it 'understands includes using nested alias namespaces resolving types' do + source = Solargraph::Source.load_string(%( + module Foo + # @return [Symbol] + def bing; :yay; end + end + + module Bar + Baz = Foo + end + + class B + include Foo + end + + a = B.new.bing + a + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [15, 8]) + expect(clip.infer.to_s).to eq('Symbol') + end + end + + describe '#get_method_stack' do + let(:out) { StringIO.new } + let(:api_map) { described_class.load_with_cache(Dir.pwd, out) } + + context 'with stdlib that has vital dependencies' do + let(:external_requires) { ['yaml'] } + let(:method_stack) { api_map.get_method_stack('YAML', 'safe_load', scope: :class) } + + it 'handles the YAML gem aliased to Psych' do + expect(method_stack).not_to be_empty + end + end + + context 'with thor' do + let(:external_requires) { ['thor'] } + let(:method_stack) { api_map.get_method_stack('Thor', 'desc', scope: :class) } + + it 'handles finding Thor.desc' do + expect(method_stack).not_to be_empty + end + end + end +end diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index c95d4d8ec..494f9e156 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -1,6 +1,7 @@ require 'tmpdir' describe Solargraph::ApiMap do + # rubocop:disable RSpec/InstanceVariable before :all do @api_map = Solargraph::ApiMap.new end @@ -817,4 +818,62 @@ def baz clip = api_map.clip_at('test.rb', [11, 10]) expect(clip.infer.to_s).to eq('Symbol') end + + it 'resolves aliases in identically named deeply nested classes' do + source = Solargraph::Source.load_string(%( + module A + module Bar + # @return [Integer] + def quux; 123; end + end + + Baz = Bar + + class Foo + include Baz + end + end + + def c + b = A::Foo.new.quux + b + end + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [16, 4]) + expect(clip.infer.to_s).to eq('Integer') + end + + it 'resolves aliases in nested classes' do + source = Solargraph::Source.load_string(%( + module A + module Bar + class Baz + # @return [Integer] + def quux; 123; end + end + end + + Baz = Bar::Baz + + class Foo + include Baz + end + end + + def c + b = A::Foo.new.quux + b + end + ), 'test.rb') + + api_map = described_class.new.map(source) + + clip = api_map.clip_at('test.rb', [18, 4]) + expect(clip.infer.to_s).to eq('Integer') + end + + # rubocop:enable RSpec/InstanceVariable end diff --git a/spec/convention/struct_definition_spec.rb b/spec/convention/struct_definition_spec.rb index fe317a42b..5c3fc5211 100644 --- a/spec/convention/struct_definition_spec.rb +++ b/spec/convention/struct_definition_spec.rb @@ -21,7 +21,7 @@ expect(param_baz.return_type.tag).to eql('Integer') end - it 'should set closure to method on assignment operator parameters' do + it 'sets closure to method on assignment operator parameters' do source = Solargraph::SourceMap.load_string(%( # @param bar [String] # @param baz [Integer] @@ -140,7 +140,7 @@ def type_checker code Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end - it 'should not crash' do + it "doesn't crash" do checker = type_checker(%( Foo = Struct.new(:bar, :baz) )) From 4a10b44b3802ea9bd077b7aaab0238b865363e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sun, 24 Aug 2025 19:46:19 +0200 Subject: [PATCH 069/524] Speed-up LSP completion response times (#1035) * Improve performance of resolve_method_aliases method - Add indexed lookups for methods and aliases by name - Cache ancestor traversal to avoid repeated computations - Separate regular pins from aliases for more efficient processing - Replace linear search with direct indexed method lookup - Add fast path for creating resolved alias pins without individual lookups Generated with Claude Code * Update .rubocop_todo.yml * Fix typechecking - get_method_stack order `get_method_stack` returns the following order for `Enumerable#select`: - master branch: => ["Enumerable#select", "Kernel#select"] - current branch: => ["Kernel#select", "Enumerable#select"] * Avoid redundant indexing methods_by_name loop through ancestors and rely on store.get_path_pins --- .rubocop_todo.yml | 2 - lib/solargraph/api_map.rb | 91 +++++++++++++++++++++------------ lib/solargraph/api_map/store.rb | 38 ++++++++++++++ spec/api_map_spec.rb | 12 ++--- 4 files changed, 103 insertions(+), 40 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 14cc0ca5d..f400dcfaf 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -77,7 +77,6 @@ Layout/ClosingHeredocIndentation: # Configuration parameters: AllowForAlignment. Layout/CommentIndentation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2656,7 +2655,6 @@ YARD/MismatchName: - 'lib/solargraph/pin/until.rb' - 'lib/solargraph/pin/while.rb' - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/source/chain.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source/chain/z_super.rb' - 'lib/solargraph/type_checker.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 9db21128f..89ed3a308 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -26,7 +26,6 @@ class ApiMap def initialize pins: [] @source_map_hash = {} @cache = Cache.new - @method_alias_stack = [] index pins end @@ -687,6 +686,7 @@ def type_include?(host_ns, module_ns) # @return [Array] def resolve_method_aliases pins, visibility = [:public, :private, :protected] with_resolved_aliases = pins.map do |pin| + next pin unless pin.is_a?(Pin::MethodAlias) resolved = resolve_method_alias(pin) next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility) resolved @@ -998,49 +998,76 @@ def prefer_non_nil_variables pins result + nil_pins end - # @param pin [Pin::MethodAlias, Pin::Base] - # @return [Pin::Method] - def resolve_method_alias pin - return pin unless pin.is_a?(Pin::MethodAlias) - return nil if @method_alias_stack.include?(pin.path) - @method_alias_stack.push pin.path - origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first - @method_alias_stack.pop - return nil if origin.nil? + include Logging + + private + + # @param alias_pin [Pin::MethodAlias] + # @return [Pin::Method, nil] + def resolve_method_alias(alias_pin) + ancestors = store.get_ancestors(alias_pin.full_context.tag) + original = nil + + # Search each ancestor for the original method + ancestors.each do |ancestor_fqns| + ancestor_fqns = ComplexType.parse(ancestor_fqns).force_rooted.namespace + ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}" + + # Search for the original method in the ancestor + original = store.get_path_pins(ancestor_method_path).find do |candidate_pin| + if candidate_pin.is_a?(Pin::MethodAlias) + # recursively resolve method aliases + resolved = resolve_method_alias(candidate_pin) + break resolved if resolved + end + + candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope + end + + break if original + end + + # @sg-ignore ignore `received nil` for original + create_resolved_alias_pin(alias_pin, original) if original + end + + # Fast path for creating resolved alias pins without individual method stack lookups + # @param alias_pin [Pin::MethodAlias] The alias pin to resolve + # @param original [Pin::Method] The original method pin that was already found + # @return [Pin::Method] The resolved method pin + def create_resolved_alias_pin(alias_pin, original) + # Build the resolved method pin directly (same logic as resolve_method_alias but without lookup) args = { - location: pin.location, - type_location: origin.type_location, - closure: pin.closure, - name: pin.name, - comments: origin.comments, - scope: origin.scope, -# context: pin.context, - visibility: origin.visibility, - signatures: origin.signatures.map(&:clone).freeze, - attribute: origin.attribute?, - generics: origin.generics.clone, - return_type: origin.return_type, + location: alias_pin.location, + type_location: original.type_location, + closure: alias_pin.closure, + name: alias_pin.name, + comments: original.comments, + scope: original.scope, + visibility: original.visibility, + signatures: original.signatures.map(&:clone).freeze, + attribute: original.attribute?, + generics: original.generics.clone, + return_type: original.return_type, source: :resolve_method_alias } - out = Pin::Method.new **args - out.signatures.each do |sig| + resolved_pin = Pin::Method.new **args + + # Clone signatures and parameters + resolved_pin.signatures.each do |sig| sig.parameters = sig.parameters.map(&:clone).freeze sig.source = :resolve_method_alias sig.parameters.each do |param| - param.closure = out + param.closure = resolved_pin param.source = :resolve_method_alias param.reset_generated! end - sig.closure = out + sig.closure = resolved_pin sig.reset_generated! end - logger.debug { "ApiMap#resolve_method_alias(pin=#{pin}) - returning #{out} from #{origin}" } - out - end - include Logging - - private + resolved_pin + end # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 3b3fffd69..6479e6039 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -204,6 +204,44 @@ def fqns_pins fqns fqns_pins_map[[base, name]] end + # Get all ancestors (superclasses, includes, prepends, extends) for a namespace + # @param fqns [String] The fully qualified namespace + # @return [Array] Array of ancestor namespaces including the original + def get_ancestors(fqns) + return [] if fqns.nil? || fqns.empty? + + ancestors = [fqns] + visited = Set.new + queue = [fqns] + + until queue.empty? + current = queue.shift + next if current.nil? || current.empty? || visited.include?(current) + visited.add(current) + + current = current.gsub(/^::/, '') + + # Add superclass + superclass = get_superclass(current) + if superclass && !superclass.empty? && !visited.include?(superclass) + ancestors << superclass + queue << superclass + end + + # Add includes, prepends, and extends + [get_includes(current), get_prepends(current), get_extends(current)].each do |refs| + next if refs.nil? + refs.each do |ref| + next if ref.nil? || ref.empty? || visited.include?(ref) + ancestors << ref + queue << ref + end + end + end + + ancestors.compact.uniq + end + private # @return [Index] diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 494f9e156..b447e3fd3 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -210,19 +210,19 @@ def prot it 'finds stacks of methods' do map = Solargraph::SourceMap.load_string(%( module Mixin - def meth; end + def select; end end - class Foo + class Foo < Array include Mixin - def meth; end + def select; end end class Bar < Foo - def meth; end + def select; end end )) @api_map.index map.pins - pins = @api_map.get_method_stack('Bar', 'meth') - expect(pins.map(&:path)).to eq(['Bar#meth', 'Foo#meth', 'Mixin#meth']) + pins = @api_map.get_method_stack('Bar', 'select') + expect(pins.map(&:path)).to eq(['Bar#select', 'Foo#select', 'Mixin#select', 'Array#select', 'Enumerable#select', 'Kernel#select']) end it 'finds symbols' do From 5517ff7a510c784a8df330f5bdd40239d49b5dd1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 09:22:20 -0400 Subject: [PATCH 070/524] Linting fixes --- lib/solargraph/shell.rb | 2 ++ lib/solargraph/type_checker.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a23e15a9b..964e21d00 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -170,6 +170,7 @@ def reporters # @return [void] def typecheck *files directory = File.realpath(options[:directory]) + # @sg-ignore Unresolved call to options level = options[:level].to_sym rules = Solargraph::TypeChecker::Rules.new(level) api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout, loose_unions: rules.loose_unions?) @@ -182,6 +183,7 @@ def typecheck *files filecount = 0 time = Benchmark.measure { files.each do |file| + # @sg-ignore Unresolved call to options checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: options[:level].to_sym) problems = checker.problems next if problems.empty? diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index aee83db0e..0a5be1f14 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -23,6 +23,7 @@ class TypeChecker # @param filename [String] # @param api_map [ApiMap, nil] + # @param rules [Rules] # @param level [Symbol] def initialize filename, api_map: nil, level: :normal, rules: Rules.new(level) @filename = filename From 6c557803da5b37cee2ecdb5b14de5804e47f8fa5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 13:02:35 -0400 Subject: [PATCH 071/524] Fix merge issue --- lib/solargraph/gem_pins.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 877eeb15d..43422505b 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -11,29 +11,20 @@ class << self include Logging end - # @param gemspec [Gem::Specification] + # @param pins [Array] # @return [Array] - def self.build_yard_pins(gemspec) - Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec) - yardoc = Yardoc.load!(gemspec) - YardMap::Mapper.new(yardoc, gemspec).map - end - - # @param pins [Array] - # @return [Array] def self.combine_method_pins_by_path(pins) method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method } by_path = method_pins.group_by(&:path) - combined_by_path = by_path.transform_values do |pins| + by_path.transform_values! do |pins| GemPins.combine_method_pins(*pins) end - combined_by_path.values + alias_pins + by_path.values + alias_pins end # @param pins [Pin::Base] # @return [Pin::Base, nil] def self.combine_method_pins(*pins) - # @type [Pin::Method, nil] out = pins.reduce(nil) do |memo, pin| next pin if memo.nil? if memo == pin && memo.source != :combined @@ -48,6 +39,15 @@ def self.combine_method_pins(*pins) out end + # @param yard_plugins [Array] The names of YARD plugins to use. + # @param gemspec [Gem::Specification] + # @return [Array] + def self.build_yard_pins(yard_plugins, gemspec) + Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec) + yardoc = Yardoc.load!(gemspec) + YardMap::Mapper.new(yardoc, gemspec).map + end + # @param yard_pins [Array] # @param rbs_map [RbsMap] # @return [Array] From d1e6b128e289ff8ef4ed8bd4c563ec297fcea486 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 13:18:41 -0400 Subject: [PATCH 072/524] RuboCop todo update --- .rubocop_todo.yml | 43 +++++++++++---------------------------- lib/solargraph/api_map.rb | 2 +- lib/solargraph/shell.rb | 5 +++++ spec/shell_spec.rb | 8 +++++--- spec/spec_helper.rb | 1 - 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f400dcfaf..d32ff61cb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,51 +1,44 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.2. +# using RuboCop version 1.80.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: **/*.gemspec Gemspec/AddRuntimeDependency: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' -# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# Configuration parameters: EnforcedStyle, AllowedGems. # SupportedStyles: Gemfile, gems.rb, gemspec -# Include: **/*.gemspec, **/Gemfile, **/gems.rb Gemspec/DevelopmentDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemspec +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. Gemspec/OrderedDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequiredRubyVersion: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' @@ -622,7 +615,7 @@ Lint/UnmodifiedReduceAccumulator: - 'lib/solargraph/pin/method.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' @@ -630,7 +623,7 @@ Lint/UnusedBlockArgument: - 'spec/language_server/transport/data_reader_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: @@ -660,13 +653,12 @@ Lint/UnusedMethodArgument: - 'spec/doc_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. Lint/UselessAccessModifier: Exclude: - 'lib/solargraph/api_map.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - 'lib/solargraph/doc_map.rb' @@ -697,7 +689,6 @@ Lint/UselessConstantScoping: - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. Lint/UselessMethodDefinition: Exclude: - 'lib/solargraph/pin/signature.rb' @@ -1009,7 +1000,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. RSpec/EmptyExampleGroup: Exclude: - 'spec/convention_spec.rb' @@ -1109,16 +1099,10 @@ RSpec/LeakyConstantDeclaration: - 'spec/complex_type_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: . -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - EnforcedStyle: receive - RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' @@ -1267,13 +1251,11 @@ RSpec/RepeatedExample: - 'spec/type_checker/levels/strict_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb +# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata. RSpec/SpecFilePathFormat: Exclude: - '**/spec/routing/**/*' @@ -1703,7 +1685,7 @@ Style/EmptyLambdaParameter: - 'spec/rbs_map/core_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: @@ -1842,7 +1824,6 @@ Style/FrozenStringLiteralComment: - 'spec/rbs_map/core_map_spec.rb' - 'spec/rbs_map/stdlib_map_spec.rb' - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - 'spec/source/chain/array_spec.rb' - 'spec/source/chain/call_spec.rb' - 'spec/source/chain/class_variable_spec.rb' @@ -2234,7 +2215,7 @@ Style/RedundantFreeze: - 'lib/solargraph/source_map/mapper.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, AllowComments. +# Configuration parameters: AllowComments. Style/RedundantInitialize: Exclude: - 'lib/solargraph/rbs_map/core_map.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 89ed3a308..9231e6a59 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -568,7 +568,7 @@ def get_path_suggestions path # Get an array of pins that match the specified path. # # @param path [String] - # @return [Enumerable] + # @return [Array] def get_path_pins path get_path_suggestions(path) end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 48d636bc2..40f5f4323 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -249,12 +249,17 @@ def list def method_pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) + # @type [Array] pins = if options[:stack] scope, ns, meth = if path.include? '#' [:instance, *path.split('#', 2)] else [:class, *path.split('.', 2)] end + + # @sg-ignore Wrong argument type for + # Solargraph::ApiMap#get_method_stack: rooted_tag + # expected String, received Array api_map.get_method_stack(ns, meth, scope: scope) else api_map.get_path_pins path diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index f2eba6a83..cdc0d09fc 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -5,7 +5,7 @@ describe Solargraph::Shell do let(:shell) { described_class.new } - + let(:temp_dir) { Dir.mktmpdir } before do @@ -42,7 +42,9 @@ def bundle_exec(*cmd) it "uncaches without erroring out" do output = bundle_exec("solargraph", "uncache", "solargraph") - expect(output).to include('Clearing pin cache in') + expect(output).to include('Clearing pin cache in') + end + end # @type cmd [Array] # @return [String] @@ -95,7 +97,7 @@ def bundle_exec(*cmd) shell.options = { stack: true } shell.method_pin('String#to_s') end - expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) + expect(api_map).to haveo_received(:get_method_stack).with('String', 'to_s', scope: :instance) end it 'prints a static pin using stack results' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3e2631976..59d107aa3 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,7 +44,6 @@ def with_env_var(name, value) end end - def capture_stdout &block original_stdout = $stdout $stdout = StringIO.new From 5b612ddee00a78208206095ec32c4bc661820df8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 13:21:21 -0400 Subject: [PATCH 073/524] Try rbs collection update before specs --- .github/workflows/rspec.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..9c0fe219e 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,6 +48,9 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version + - name: Update types + run: | + bundle update rbs collection update - name: Run tests run: bundle exec rake spec undercover: From 809ad2711ecbbfef05941192d6a7380fd8e53076 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 13:24:26 -0400 Subject: [PATCH 074/524] Fix merge issue --- lib/solargraph/type_checker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 2b791e425..0b43c44fe 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -441,7 +441,7 @@ def signature_argument_problems_for location, locals, closure_pin, params, argum # @todo Some level (strong, I guess) should require the param here else argtype = argchain.infer(api_map, closure_pin, locals) - if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype) + if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype) errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") return errors end From f2291f4eadf8cf69b4f9f6baa18908a71e40cbbe Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 16:14:24 -0400 Subject: [PATCH 075/524] RuboCop fixes --- spec/doc_map_spec.rb | 3 ++- spec/language_server/host/diagnoser_spec.rb | 3 ++- spec/language_server/host/message_worker_spec.rb | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b03e573f0..a13308bf6 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -42,8 +42,9 @@ 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).with(/path set/) + allow(Solargraph.logger).to receive(:warn).and_call_original Solargraph::DocMap.new(['set'], []) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end it 'ignores nil requires' do diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index d59a843f1..69ee0b866 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -3,7 +3,8 @@ host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) diagnoser.schedule 'file.rb' - expect(host).to receive(:diagnose).with('file.rb') + allow(host).to receive(:diagnose) diagnoser.tick + expect(host).to have_received(:diagnose).with('file.rb') end end diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index b9ce2a41f..5e5bef481 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,11 +2,12 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - expect(host).to receive(:receive).with(message).and_return(nil) + allow(host).to receive(:receive).with(message).and_return(nil) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick + expect(host).to have_received(:receive).with(message) end end From ed1c54e12abcf12fc9ab2ccf74d948c6984ecf0f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 16:21:27 -0400 Subject: [PATCH 076/524] Rebaseline Rubocop todo --- .rubocop.yml | 3 + .rubocop_todo.yml | 283 ++++++++++++++-------------------------------- 2 files changed, 86 insertions(+), 200 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..c17a56410 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,9 @@ AllCops: - "vendor/**/.*" TargetRubyVersion: 3.0 +RSpec/SpecFilePathFormat: + Enabled: false + Style/MethodDefParentheses: EnforcedStyle: require_no_parentheses diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f400dcfaf..4bdf2c073 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,51 +1,44 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.2. +# using RuboCop version 1.80.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: **/*.gemspec Gemspec/AddRuntimeDependency: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/DeprecatedAttributeAssignment: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' -# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# Configuration parameters: EnforcedStyle, AllowedGems. # SupportedStyles: Gemfile, gems.rb, gemspec -# Include: **/*.gemspec, **/Gemfile, **/gems.rb Gemspec/DevelopmentDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemspec +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. Gemspec/OrderedDependencies: Exclude: - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequiredRubyVersion: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' @@ -71,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -233,7 +225,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -277,6 +268,77 @@ Layout/LineContinuationSpacing: Exclude: - 'lib/solargraph/diagnostics/rubocop_helpers.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Exclude: + - 'lib/solargraph/api_map.rb' + - 'lib/solargraph/api_map/source_to_yard.rb' + - 'lib/solargraph/api_map/store.rb' + - 'lib/solargraph/complex_type.rb' + - 'lib/solargraph/complex_type/unique_type.rb' + - 'lib/solargraph/convention/data_definition.rb' + - 'lib/solargraph/doc_map.rb' + - 'lib/solargraph/gem_pins.rb' + - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' + - 'lib/solargraph/language_server/message/extended/download_core.rb' + - 'lib/solargraph/language_server/message/initialize.rb' + - 'lib/solargraph/language_server/message/text_document/completion.rb' + - 'lib/solargraph/language_server/message/text_document/definition.rb' + - 'lib/solargraph/language_server/message/text_document/document_highlight.rb' + - 'lib/solargraph/language_server/message/text_document/prepare_rename.rb' + - 'lib/solargraph/language_server/message/text_document/references.rb' + - 'lib/solargraph/language_server/message/text_document/rename.rb' + - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' + - 'lib/solargraph/library.rb' + - 'lib/solargraph/parser/comment_ripper.rb' + - 'lib/solargraph/parser/flow_sensitive_typing.rb' + - 'lib/solargraph/parser/parser_gem/node_chainer.rb' + - 'lib/solargraph/parser/parser_gem/node_methods.rb' + - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' + - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' + - 'lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb' + - 'lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb' + - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' + - 'lib/solargraph/pin/base.rb' + - 'lib/solargraph/pin/callable.rb' + - 'lib/solargraph/pin/common.rb' + - 'lib/solargraph/pin/documenting.rb' + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/pin/parameter.rb' + - 'lib/solargraph/rbs_map/conversions.rb' + - 'lib/solargraph/rbs_map/core_fills.rb' + - 'lib/solargraph/shell.rb' + - 'lib/solargraph/source.rb' + - 'lib/solargraph/source/chain.rb' + - 'lib/solargraph/source/chain/call.rb' + - 'lib/solargraph/source/chain/if.rb' + - 'lib/solargraph/source/chain/instance_variable.rb' + - 'lib/solargraph/source/chain/variable.rb' + - 'lib/solargraph/source/cursor.rb' + - 'lib/solargraph/source/encoding_fixes.rb' + - 'lib/solargraph/source/source_chainer.rb' + - 'lib/solargraph/source_map.rb' + - 'lib/solargraph/source_map/clip.rb' + - 'lib/solargraph/source_map/mapper.rb' + - 'lib/solargraph/type_checker.rb' + - 'lib/solargraph/workspace.rb' + - 'lib/solargraph/workspace/config.rb' + - 'lib/solargraph/yard_map/mapper/to_method.rb' + - 'spec/api_map_spec.rb' + - 'spec/complex_type_spec.rb' + - 'spec/language_server/message/completion_item/resolve_spec.rb' + - 'spec/language_server/message/extended/check_gem_version_spec.rb' + - 'spec/language_server/message/text_document/definition_spec.rb' + - 'spec/language_server/protocol_spec.rb' + - 'spec/pin/parameter_spec.rb' + - 'spec/source/chain_spec.rb' + - 'spec/source_map/clip_spec.rb' + - 'spec/source_map_spec.rb' + - 'spec/workspace_spec.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line @@ -622,7 +684,7 @@ Lint/UnmodifiedReduceAccumulator: - 'lib/solargraph/pin/method.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' @@ -630,7 +692,7 @@ Lint/UnusedBlockArgument: - 'spec/language_server/transport/data_reader_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: @@ -660,13 +722,12 @@ Lint/UnusedMethodArgument: - 'spec/doc_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. Lint/UselessAccessModifier: Exclude: - 'lib/solargraph/api_map.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - 'lib/solargraph/doc_map.rb' @@ -697,7 +758,6 @@ Lint/UselessConstantScoping: - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. Lint/UselessMethodDefinition: Exclude: - 'lib/solargraph/pin/signature.rb' @@ -998,7 +1058,6 @@ RSpec/DescribedClass: - 'spec/source_map/mapper_spec.rb' - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/rules_spec.rb' @@ -1009,7 +1068,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. RSpec/EmptyExampleGroup: Exclude: - 'spec/convention_spec.rb' @@ -1109,7 +1167,6 @@ RSpec/LeakyConstantDeclaration: - 'spec/complex_type_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' @@ -1179,7 +1236,6 @@ RSpec/MultipleExpectations: - 'spec/source_map/node_processor_spec.rb' - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' @@ -1204,7 +1260,6 @@ RSpec/NoExpectationExample: - 'spec/pin/block_spec.rb' - 'spec/pin/method_spec.rb' - 'spec/source/chain/call_spec.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/typed_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -1267,109 +1322,10 @@ RSpec/RepeatedExample: - 'spec/type_checker/levels/strict_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect. RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. -# Include: **/*_spec.rb -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/api_map_method_spec.rb' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/language_server/host/message_worker_spec.rb' @@ -1703,7 +1659,7 @@ Style/EmptyLambdaParameter: - 'spec/rbs_map/core_map_spec.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: @@ -1864,7 +1820,6 @@ Style/FrozenStringLiteralComment: - 'spec/source_map_spec.rb' - 'spec/source_spec.rb' - 'spec/spec_helper.rb' - - 'spec/type_checker/checks_spec.rb' - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' @@ -1989,7 +1944,6 @@ Style/MapIntoArray: Exclude: - 'lib/solargraph/diagnostics/update_errors.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/type_checker/param_def.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: @@ -2056,7 +2010,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/source_map.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/type_checker/checks.rb' - 'lib/solargraph/yard_map/helpers.rb' - 'lib/solargraph/yardoc.rb' - 'spec/doc_map_spec.rb' @@ -2139,7 +2092,6 @@ Style/Next: - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - 'lib/solargraph/pin/signature.rb' - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. @@ -2234,7 +2186,7 @@ Style/RedundantFreeze: - 'lib/solargraph/source_map/mapper.rb' # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, AllowComments. +# Configuration parameters: AllowComments. Style/RedundantInitialize: Exclude: - 'lib/solargraph/rbs_map/core_map.rb' @@ -2355,7 +2307,6 @@ Style/SlicingWithRange: - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source/source_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. @@ -2661,77 +2612,9 @@ YARD/MismatchName: YARD/TagTypeSyntax: Exclude: - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/type_checker.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. -# URISchemes: http, https -Layout/LineLength: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/complex_type.rb' + - 'lib/solargraph/complex_type/conformance.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/language_server/message/extended/download_core.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/language_server/message/text_document/definition.rb' - - 'lib/solargraph/language_server/message/text_document/document_highlight.rb' - - 'lib/solargraph/language_server/message/text_document/prepare_rename.rb' - - 'lib/solargraph/language_server/message/text_document/references.rb' - - 'lib/solargraph/language_server/message/text_document/rename.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/common.rb' - - 'lib/solargraph/pin/documenting.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/rbs_map/core_fills.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/if.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - - 'lib/solargraph/workspace/config.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/workspace_spec.rb' From 260f2270bc743c7e3b1d25183a8f706dc8463e8e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 16:21:48 -0400 Subject: [PATCH 077/524] Fix RuboCop issue --- lib/solargraph/rbs_map/core_map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 5e030d9f6..bff943764 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -28,7 +28,7 @@ def pins @pins.concat RbsMap::CoreFills::ALL # process overrides, then remove any which couldn't be resolved processed = ApiMap::Store.new(@pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } - STDOUT.puts "RBS core pins cache size: #{@pins.size}" + puts "RBS core pins cache size: #{@pins.size}" @pins.replace processed PinCache.serialize_core @pins From be46aa30a4951813535e34f3b540fde75a5fb12f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 16:26:40 -0400 Subject: [PATCH 078/524] Add @sg-ignores --- lib/solargraph/shell.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 40f5f4323..0200cc624 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -239,16 +239,22 @@ def list puts "#{workspace.filenames.length} files total." end + # @sg-ignore Unresolved call to desc desc 'method_pin [PATH]', 'Describe a method pin' + # @sg-ignore Unresolved call to option option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false + # @sg-ignore Unresolved call to option option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false + # @sg-ignore Unresolved call to option option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false + # @sg-ignore Unresolved call to option option :stack, type: :boolean, desc: 'Show entire stack by including definitions in superclasses', default: false # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' # @return [void] def method_pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) + # @sg-ignore Unresolved call to options # @type [Array] pins = if options[:stack] scope, ns, meth = if path.include? '#' @@ -269,9 +275,12 @@ def method_pin path exit 1 end pins.each do |pin| + # @sg-ignore Unresolved call to options if options[:typify] || options[:probe] type = ComplexType::UNDEFINED + # @sg-ignore Unresolved call to options type = pin.typify(api_map) if options[:typify] + # @sg-ignore Unresolved call to options type = pin.probe(api_map) if options[:probe] && type.undefined? print_type(type) next @@ -311,6 +320,7 @@ def do_cache gemspec, api_map # @param type [ComplexType] # @return [void] def print_type(type) + # @sg-ignore Unresolved call to options if options[:rbs] puts type.to_rbs else @@ -321,6 +331,7 @@ def print_type(type) # @param pin [Solargraph::Pin::Base] # @return [void] def print_pin(pin) + # @sg-ignore Unresolved call to options if options[:rbs] puts pin.to_rbs else From 0927309cf2d4ff598feff0058fc04bcd9e44bad0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:11:04 -0400 Subject: [PATCH 079/524] Fix typo --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 9c0fe219e..8f6dac24d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -50,7 +50,7 @@ jobs: bundle update rbs # use latest available for this Ruby version - name: Update types run: | - bundle update rbs collection update + bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: From 78995508e6bf35ce1e8c9b2ee4693e71bec52981 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:15:16 -0400 Subject: [PATCH 080/524] Exclude problematic combinations on Ruby head --- .github/workflows/rspec.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 8f6dac24d..b06f1425e 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,7 +29,15 @@ jobs: rbs-version: '3.9.4' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' - steps: + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS release + - ruby-version: 'head' + rbs-version: '4.0.0.dev.4' + - ruby-version: 'head' + rbs-version: '3.9.4' + - ruby-version: 'head' + rbs-version: '3.6.1' + steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 From d5d6c5fb74415521d5daab6e91aa9ff4e19ada57 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:20:49 -0400 Subject: [PATCH 081/524] Fix indentation --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index b06f1425e..7e2c4b9c9 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -37,7 +37,7 @@ jobs: rbs-version: '3.9.4' - ruby-version: 'head' rbs-version: '3.6.1' - steps: + steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 From 2c6cacd9a2e9c41bdf6a5085e4d263017a392498 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:28:18 -0400 Subject: [PATCH 082/524] method_pin -> pin, hide command --- lib/solargraph/shell.rb | 6 +++--- spec/shell_spec.rb | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 0200cc624..21a53172f 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -240,7 +240,7 @@ def list end # @sg-ignore Unresolved call to desc - desc 'method_pin [PATH]', 'Describe a method pin' + desc 'pin [PATH]', 'Describe a pin', hide: true # @sg-ignore Unresolved call to option option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false # @sg-ignore Unresolved call to option @@ -248,10 +248,10 @@ def list # @sg-ignore Unresolved call to option option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false # @sg-ignore Unresolved call to option - option :stack, type: :boolean, desc: 'Show entire stack by including definitions in superclasses', default: false + option :stack, type: :boolean, desc: 'Show entire stack of a method pin by including definitions in superclasses', default: false # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' # @return [void] - def method_pin path + def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) # @sg-ignore Unresolved call to options diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index cdc0d09fc..e3c85c6e0 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -57,7 +57,7 @@ def bundle_exec(*cmd) end end - describe 'method_pin' do + describe 'pin' do let(:api_map) { instance_double(Solargraph::ApiMap) } let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } @@ -70,7 +70,7 @@ def bundle_exec(*cmd) it 'prints a pin' do allow(to_s_pin).to receive(:inspect).and_return('pin inspect result') - out = capture_both { shell.method_pin('String#to_s') } + out = capture_both { shell.pin('String#to_s') } expect(out).to eq("pin inspect result\n") end @@ -82,7 +82,7 @@ def bundle_exec(*cmd) out = capture_both do shell.options = { rbs: true } - shell.method_pin('String#to_s') + shell.pin('String#to_s') end expect(out).to eq("pin RBS result\n") end @@ -95,9 +95,9 @@ def bundle_exec(*cmd) allow(api_map).to receive(:get_method_stack).and_return([to_s_pin]) capture_both do shell.options = { stack: true } - shell.method_pin('String#to_s') + shell.pin('String#to_s') end - expect(api_map).to haveo_received(:get_method_stack).with('String', 'to_s', scope: :instance) + expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) end it 'prints a static pin using stack results' do @@ -107,7 +107,7 @@ def bundle_exec(*cmd) allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) capture_both do shell.options = { stack: true } - shell.method_pin('String.new') + shell.pin('String.new') end expect(api_map).to have_received(:get_method_stack).with('String', 'new', scope: :class) end @@ -119,7 +119,7 @@ def bundle_exec(*cmd) out = capture_both do shell.options = { typify: true } - shell.method_pin('String#to_s') + shell.pin('String#to_s') end expect(out).to eq("::String\n") end @@ -131,7 +131,7 @@ def bundle_exec(*cmd) out = capture_both do shell.options = { typify: true, rbs: true } - shell.method_pin('String#to_s') + shell.pin('String#to_s') end expect(out).to eq("::String\n") end @@ -143,7 +143,7 @@ def bundle_exec(*cmd) out = capture_both do shell.options = {} - shell.method_pin('Not#found') + shell.pin('Not#found') rescue SystemExit # Ignore the SystemExit raised by the shell when no pin is found end From ce2a106e0be8e7b50ed9ebfd95918d2a03ed6799 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:37:26 -0400 Subject: [PATCH 083/524] Drop one of two contradictory strategies --- lib/solargraph/complex_type/conformance.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/solargraph/complex_type/conformance.rb b/lib/solargraph/complex_type/conformance.rb index 424413038..63abad967 100644 --- a/lib/solargraph/complex_type/conformance.rb +++ b/lib/solargraph/complex_type/conformance.rb @@ -39,9 +39,6 @@ def conforms_to_unique_type? # :nocov: end - if use_simplified_inferred_type? - return with_new_types(inferred.simplify_literals, expected).conforms_to_unique_type? - end return true if ignore_interface? return true if conforms_via_reverse_match? @@ -79,10 +76,6 @@ def conforms_to_unique_type? private - def use_simplified_inferred_type? - inferred.simplifyable_literal? && !expected.literal? - end - def only_inferred_parameters? !expected.parameters? && inferred.parameters? end From 56636ae7022c3cc9c47b729e25c42d816243bdb9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:40:15 -0400 Subject: [PATCH 084/524] Fix type --- 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 9231e6a59..4e8332080 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -559,7 +559,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, # @deprecated Use #get_path_pins instead. # # @param path [String] The path to find - # @return [Enumerable] + # @return [Array] def get_path_suggestions path return [] if path.nil? resolve_method_aliases store.get_path_pins(path) From f7e9e606757bbeac8047f154e5b2d63b6884e74c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:47:51 -0400 Subject: [PATCH 085/524] Type fixes --- lib/solargraph/api_map/store.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 30ef9bdfc..2ac7fdf95 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -283,17 +283,17 @@ def superclass_references index.superclass_references end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references index.include_references end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references index.prepend_references end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references index.extend_references end From c96bee732931300a33b986ae158e6afe8b31cfc5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 17:59:32 -0400 Subject: [PATCH 086/524] Fix type issue --- 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 e8619b49e..b39bc72bc 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -138,7 +138,7 @@ def self.all_rbs_collection_gems_in_memory @rbs_collection_gems_in_memory ||= {} end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version + # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version def yard_pins_in_memory self.class.all_yard_gems_in_memory end From 6acfa0c54a806da203daf30765c7a99a67066f16 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 30 Aug 2025 09:06:14 -0400 Subject: [PATCH 087/524] RuboCop todo file stability To avoid merge conflicts and contributors having to deal with non-intuitive RuboCop todo changes: * Lock down development versions of RuboCop and plugins so that unrelated PRs aren't affected by newly implemented RuboCop rules. * Exclude rule entirely if more than 5 files violate it today, so that PRs are less likely to cause todo file changes unless they are specifically targeted at cleanup. * Clarify guidance on RuboCop todo file in CI error message. * Fix to hopefully ensure guidance always appears in CI error message. --- .github/workflows/linting.yml | 6 +- .rubocop.yml | 1 - .rubocop_todo.yml | 1588 ++------------------------------- solargraph.gemspec | 13 +- 4 files changed, 98 insertions(+), 1510 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 8abbf51ef..aa22ce22c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -115,11 +115,13 @@ jobs: - name: Run RuboCop against todo file continue-on-error: true run: | - bundle exec rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp + cmd="bundle exec rubocop --auto-gen-config --exclude-limit=5 --no-offense-counts --no-auto-gen-timestampxb*com" + ${cmd:?} + set +e if [ -n "$(git status --porcelain)" ] then git status --porcelain git diff -u . - >&2 echo "Please fix deltas if bad or run 'bundle exec rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp' and push up changes if good" + >&2 echo "Please address any new issues, then run '${cmd:?}' and push up any improvements" exit 1 fi diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..c7643c3c6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -34,7 +34,6 @@ Metrics/ParameterLists: Max: 7 CountKeywordArgs: false - # we tend to use @@ and the risk doesn't seem high Style/ClassVars: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f3f0069f3..bf6b2272a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,5 +1,5 @@ # This configuration was generated by -# `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# `rubocop --auto-gen-config --exclude-limit 5 --no-offense-counts --no-auto-gen-timestamp` # using RuboCop version 1.79.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. @@ -83,23 +83,7 @@ Layout/CommentIndentation: # This cop supports safe autocorrection (--autocorrect). Layout/ElseAlignment: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker/rules.rb' - - 'lib/solargraph/yard_map/mapper.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. @@ -111,18 +95,7 @@ Layout/EmptyLineBetweenDefs: # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLines: - Exclude: - - 'lib/solargraph/bench.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/pin/delegated_method.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'spec/complex_type_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -142,23 +115,7 @@ Layout/EmptyLinesAroundModuleBody: # Configuration parameters: EnforcedStyleAlignWith, Severity. # SupportedStylesAlignWith: keyword, variable, start_of_line Layout/EndAlignment: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker/rules.rb' - - 'lib/solargraph/yard_map/mapper.rb' + Enabled: false # Configuration parameters: EnforcedStyle. # SupportedStyles: native, lf, crlf @@ -172,13 +129,7 @@ Layout/EndOfLine: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/type_checker.rb' - - 'spec/spec_helper.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -203,22 +154,7 @@ Layout/FirstArrayElementIndentation: # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/FirstHashElementIndentation: - Exclude: - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/language_server/message/extended/document_gems.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/language_server/message/text_document/rename.rb' - - 'lib/solargraph/language_server/message/text_document/signature_help.rb' - - 'lib/solargraph/pin/base_variable.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. @@ -239,27 +175,7 @@ Layout/HeredocIndentation: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Width, AllowedPatterns. Layout/IndentationWidth: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/type_checker/rules.rb' - - 'lib/solargraph/yard_map/mapper.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source_map/mapper_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment, AllowRBSInlineAnnotation, AllowSteepAnnotation. @@ -289,14 +205,7 @@ Layout/MultilineMethodCallBraceLayout: # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/diagnostics/type_check.rb' - - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/pin/search.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -316,13 +225,7 @@ Layout/SpaceAfterComma: # Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space Layout/SpaceAroundEqualsInParameterDefault: - Exclude: - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/base_variable.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/pin/parameter.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAroundKeyword: @@ -334,56 +237,14 @@ Layout/SpaceAroundKeyword: # SupportedStylesForExponentOperator: space, no_space # SupportedStylesForRationalLiterals: space, no_space Layout/SpaceAroundOperators: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/workspace/config.rb' - - 'spec/library_spec.rb' - - 'spec/yard_map/mapper_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceBeforeBlockBraces: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/class_variable.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source/chain/global_variable.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/workspace/config.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/library_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Layout/SpaceBeforeComma: @@ -395,28 +256,7 @@ Layout/SpaceBeforeComma: # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/class_variable.rb' - - 'lib/solargraph/source/chain/global_variable.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/library_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. @@ -455,13 +295,7 @@ Lint/AmbiguousBlockAssociation: # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperator: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/parser/parser_gem/class_methods.rb' - - 'lib/solargraph/pin/constant.rb' - - 'lib/solargraph/pin/method.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperatorPrecedence: @@ -472,15 +306,7 @@ Lint/AmbiguousOperatorPrecedence: # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: RequireParenthesesForMethodChains. Lint/AmbiguousRange: - Exclude: - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'spec/library_spec.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. @@ -514,14 +340,7 @@ Lint/DuplicateBranch: - 'lib/solargraph/rbs_map/conversions.rb' Lint/DuplicateMethods: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/location.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/signature.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/rbs_map/core_map.rb' - - 'lib/solargraph/source/chain/link.rb' + Enabled: false # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: @@ -530,13 +349,7 @@ Lint/EmptyBlock: # Configuration parameters: AllowComments. Lint/EmptyClass: - Exclude: - - 'spec/fixtures/rubocop-validation-error/app.rb' - - 'spec/fixtures/workspace-with-gemfile/lib/other.rb' - - 'spec/fixtures/workspace/lib/other.rb' - - 'spec/fixtures/workspace/lib/something.rb' - - 'spec/fixtures/workspace_folders/folder1/app.rb' - - 'spec/fixtures/workspace_folders/folder2/app.rb' + Enabled: false # Configuration parameters: AllowComments. Lint/EmptyFile: @@ -635,31 +448,7 @@ Lint/UnusedBlockArgument: # Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. # NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/convention/base.rb' - - 'lib/solargraph/diagnostics/base.rb' - - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/block_symbol.rb' - - 'lib/solargraph/source/chain/block_variable.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/class_variable.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source/chain/global_variable.rb' - - 'lib/solargraph/source/chain/hash.rb' - - 'lib/solargraph/source/chain/head.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/link.rb' - - 'lib/solargraph/source/chain/literal.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/chain/z_super.rb' - - 'spec/doc_map_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. @@ -670,30 +459,7 @@ Lint/UselessAccessModifier: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AutoCorrect. Lint/UselessAssignment: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/language_server/message/extended/document_gems.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'spec/fixtures/long_squiggly_heredoc.rb' - - 'spec/fixtures/rubocop-unused-variable-error/app.rb' - - 'spec/fixtures/unicode.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/library_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source_map/mapper_spec.rb' + Enabled: false Lint/UselessConstantScoping: Exclude: @@ -707,43 +473,16 @@ Lint/UselessMethodDefinition: # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false -# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/type_checker.rb' + Max: 54 -# Configuration parameters: CountBlocks, CountModifierForms, Max. +# Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: - Exclude: - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker.rb' + Max: 5 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: @@ -755,33 +494,15 @@ Metrics/ClassLength: # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source_map/mapper.rb' + Enabled: false -# Configuration parameters: CountComments, Max, CountAsOne. +# Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Exclude: - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin_cache.rb' + Max: 169 # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: @@ -794,13 +515,7 @@ Metrics/ParameterLists: # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false Naming/AccessorMethodName: Exclude: @@ -823,41 +538,18 @@ Naming/HeredocDelimiterNaming: # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: - Exclude: - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/convention/gemfile.rb' - - 'lib/solargraph/convention/gemspec.rb' - - 'lib/solargraph/convention/rakefile.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/workspace.rb' + Enabled: false # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: - Exclude: - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_map/to_method.rb' + Enabled: false # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. # AllowedMethods: call # WaywardPredicates: nonzero? Naming/PredicateMethod: - Exclude: - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/language_server/progress.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/node_processor/base.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/workspace.rb' + Enabled: false # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. # NamePrefix: is_, has_, have_, does_ @@ -912,16 +604,7 @@ RSpec/BeforeAfterAll: # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: - Exclude: - - 'spec/complex_type_spec.rb' - - 'spec/library_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' + Enabled: false # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -938,81 +621,11 @@ RSpec/DescribeClass: # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. # SupportedStyles: described_class, explicit RSpec/DescribedClass: - Exclude: - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect. -RSpec/EmptyExampleGroup: + Enabled: false + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AutoCorrect. +RSpec/EmptyExampleGroup: Exclude: - 'spec/convention_spec.rb' @@ -1023,33 +636,7 @@ RSpec/EmptyLineAfterFinalLet: # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: - Exclude: - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/library_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. @@ -1077,15 +664,7 @@ RSpec/ExpectActual: # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, each, example RSpec/HookArgument: - Exclude: - - 'spec/api_map/config_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: . @@ -1095,13 +674,7 @@ RSpec/ImplicitExpect: # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: - Exclude: - - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/protocol_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). RSpec/LeadingSubject: @@ -1127,89 +700,17 @@ RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' -# Configuration parameters: Max. RSpec/MultipleExpectations: - Exclude: - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/library_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_chainer_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map/node_processor_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' + Max: 14 -# Configuration parameters: Max, AllowedGroups. +# Configuration parameters: AllowedGroups. RSpec/NestedGroups: - Exclude: - - 'spec/complex_type_spec.rb' + Max: 4 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: - Exclude: - - 'spec/language_server/protocol_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -1220,18 +721,7 @@ RSpec/NotToNot: - 'spec/rbs_map/core_map_spec.rb' RSpec/PendingWithoutReason: - Exclude: - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/parser/node_chainer_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. @@ -1251,16 +741,7 @@ RSpec/RemoveConst: - 'spec/diagnostics/rubocop_helpers_spec.rb' RSpec/RepeatedDescription: - Exclude: - - 'spec/api_map_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' + Enabled: false RSpec/RepeatedExample: Exclude: @@ -1279,99 +760,7 @@ RSpec/ScatteredLet: # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. # Include: **/*_spec.rb RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' + Enabled: false RSpec/StubbedMock: Exclude: @@ -1379,20 +768,7 @@ RSpec/StubbedMock: # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: - Exclude: - - 'spec/complex_type_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/workspace_spec.rb' + Enabled: false Security/MarshalLoad: Exclude: @@ -1402,20 +778,7 @@ Security/MarshalLoad: # Configuration parameters: EnforcedStyle, AllowModifiersOnSymbols, AllowModifiersOnAttrs, AllowModifiersOnAliasMethod. # SupportedStyles: inline, group Style/AccessModifierDeclarations: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/location.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/position.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/hash.rb' - - 'lib/solargraph/source/chain/if.rb' - - 'lib/solargraph/source/chain/link.rb' - - 'lib/solargraph/source/chain/literal.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -1431,21 +794,7 @@ Style/AccessorGrouping: # Configuration parameters: EnforcedStyle. # SupportedStyles: always, conditionals Style/AndOr: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/language_server/message/base.rb' - - 'lib/solargraph/page.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/position.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source/updater.rb' - - 'lib/solargraph/workspace.rb' - - 'lib/solargraph/yard_map/mapper.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. @@ -1464,44 +813,12 @@ Style/ArgumentsForwarding: # FunctionalMethods: let, let!, subject, watch # AllowedMethods: lambda, proc, it Style/BlockDelimiters: - Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/library_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/position_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: MinBranchesCount. Style/CaseLikeIf: - Exclude: - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/yard_map/mapper.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. @@ -1509,18 +826,7 @@ Style/CaseLikeIf: # SupportedStylesForClasses: ~, nested, compact # SupportedStylesForModules: ~, nested, compact Style/ClassAndModuleChildren: - Exclude: - - 'lib/solargraph/language_server/message/text_document/definition.rb' - - 'lib/solargraph/language_server/message/text_document/document_highlight.rb' - - 'lib/solargraph/language_server/message/text_document/document_symbol.rb' - - 'lib/solargraph/language_server/message/text_document/prepare_rename.rb' - - 'lib/solargraph/language_server/message/text_document/references.rb' - - 'lib/solargraph/language_server/message/text_document/rename.rb' - - 'lib/solargraph/language_server/message/text_document/type_definition.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_configuration.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' - - 'lib/solargraph/language_server/message/workspace/workspace_symbol.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -1556,150 +862,13 @@ Style/ConcatArrayLiterals: # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Exclude: + - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/parser/parser_gem/node_processors/defs_node.rb' - 'lib/solargraph/source/chain/call.rb' # Configuration parameters: AllowedConstants. Style/Documentation: - Exclude: - - 'spec/**/*' - - 'test/**/*' - - 'lib/solargraph/api_map/cache.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/convention/gemfile.rb' - - 'lib/solargraph/convention/gemspec.rb' - - 'lib/solargraph/convention/rakefile.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/converters/dd.rb' - - 'lib/solargraph/converters/dl.rb' - - 'lib/solargraph/converters/dt.rb' - - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/language_server/message/base.rb' - - 'lib/solargraph/language_server/message/cancel_request.rb' - - 'lib/solargraph/language_server/message/client.rb' - - 'lib/solargraph/language_server/message/client/register_capability.rb' - - 'lib/solargraph/language_server/message/completion_item.rb' - - 'lib/solargraph/language_server/message/exit_notification.rb' - - 'lib/solargraph/language_server/message/extended/document.rb' - - 'lib/solargraph/language_server/message/extended/search.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/language_server/message/initialized.rb' - - 'lib/solargraph/language_server/message/method_not_found.rb' - - 'lib/solargraph/language_server/message/method_not_implemented.rb' - - 'lib/solargraph/language_server/message/shutdown.rb' - - 'lib/solargraph/language_server/message/text_document.rb' - - 'lib/solargraph/language_server/message/text_document/base.rb' - - 'lib/solargraph/language_server/message/text_document/code_action.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/language_server/message/text_document/definition.rb' - - 'lib/solargraph/language_server/message/text_document/did_change.rb' - - 'lib/solargraph/language_server/message/text_document/did_close.rb' - - 'lib/solargraph/language_server/message/text_document/did_open.rb' - - 'lib/solargraph/language_server/message/text_document/did_save.rb' - - 'lib/solargraph/language_server/message/text_document/document_highlight.rb' - - 'lib/solargraph/language_server/message/text_document/document_symbol.rb' - - 'lib/solargraph/language_server/message/text_document/folding_range.rb' - - 'lib/solargraph/language_server/message/text_document/formatting.rb' - - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/language_server/message/text_document/on_type_formatting.rb' - - 'lib/solargraph/language_server/message/text_document/prepare_rename.rb' - - 'lib/solargraph/language_server/message/text_document/references.rb' - - 'lib/solargraph/language_server/message/text_document/rename.rb' - - 'lib/solargraph/language_server/message/text_document/signature_help.rb' - - 'lib/solargraph/language_server/message/text_document/type_definition.rb' - - 'lib/solargraph/language_server/message/workspace.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_configuration.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb' - - 'lib/solargraph/language_server/message/workspace/workspace_symbol.rb' - - 'lib/solargraph/language_server/request.rb' - - 'lib/solargraph/language_server/transport/data_reader.rb' - - 'lib/solargraph/logging.rb' - - 'lib/solargraph/page.rb' - - 'lib/solargraph/parser.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - - 'lib/solargraph/parser/node_processor/base.rb' - - 'lib/solargraph/parser/parser_gem.rb' - - 'lib/solargraph/parser/parser_gem/class_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/alias_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/args_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/begin_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/cvasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/def_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/defs_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/gvasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/sym_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/until_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/while_node.rb' - - 'lib/solargraph/parser/snippet.rb' - - 'lib/solargraph/pin/base_variable.rb' - - 'lib/solargraph/pin/block.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/common.rb' - - 'lib/solargraph/pin/constant.rb' - - 'lib/solargraph/pin/instance_variable.rb' - - 'lib/solargraph/pin/keyword.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/pin/proxy_type.rb' - - 'lib/solargraph/pin/reference.rb' - - 'lib/solargraph/pin/reference/override.rb' - - 'lib/solargraph/pin/reference/require.rb' - - 'lib/solargraph/pin/search.rb' - - 'lib/solargraph/pin/signature.rb' - - 'lib/solargraph/pin/singleton.rb' - - 'lib/solargraph/pin/symbol.rb' - - 'lib/solargraph/pin/until.rb' - - 'lib/solargraph/pin/while.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/server_methods.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/chain/array.rb' - - 'lib/solargraph/source/chain/block_symbol.rb' - - 'lib/solargraph/source/chain/block_variable.rb' - - 'lib/solargraph/source/chain/class_variable.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source/chain/global_variable.rb' - - 'lib/solargraph/source/chain/hash.rb' - - 'lib/solargraph/source/chain/if.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/link.rb' - - 'lib/solargraph/source/chain/literal.rb' - - 'lib/solargraph/source/chain/or.rb' - - 'lib/solargraph/source/chain/q_call.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/chain/z_super.rb' - - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/source_map/data.rb' - - 'lib/solargraph/yard_map/cache.rb' - - 'lib/solargraph/yard_map/helpers.rb' - - 'lib/solargraph/yard_map/mapper.rb' - - 'lib/solargraph/yard_map/mapper/to_constant.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_map/mapper/to_namespace.rb' - - 'lib/solargraph/yard_map/to_method.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/EmptyLambdaParameter: @@ -1750,136 +919,7 @@ Style/FloatDivision: # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: - Exclude: - - '**/*.arb' - - 'Gemfile' - - 'Rakefile' - - 'bin/solargraph' - - 'lib/solargraph/converters/dd.rb' - - 'lib/solargraph/converters/dl.rb' - - 'lib/solargraph/converters/dt.rb' - - 'lib/solargraph/converters/misc.rb' - - 'lib/solargraph/parser.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - - 'lib/solargraph/parser/parser_gem.rb' - - 'lib/solargraph/parser/snippet.rb' - - 'lib/solargraph/pin/breakable.rb' - - 'lib/solargraph/pin/signature.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/source/chain/array.rb' - - 'lib/solargraph/source/chain/q_call.rb' - - 'lib/solargraph/yard_map/helpers.rb' - - 'solargraph.gemspec' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/fixtures/formattable.rb' - - 'spec/fixtures/long_squiggly_heredoc.rb' - - 'spec/fixtures/rdoc-lib/Gemfile' - - 'spec/fixtures/rdoc-lib/lib/example.rb' - - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' - - 'spec/fixtures/rubocop-validation-error/app.rb' - - 'spec/fixtures/unicode.rb' - - 'spec/fixtures/workspace-with-gemfile/Gemfile' - - 'spec/fixtures/workspace-with-gemfile/app.rb' - - 'spec/fixtures/workspace-with-gemfile/lib/other.rb' - - 'spec/fixtures/workspace-with-gemfile/lib/thing.rb' - - 'spec/fixtures/workspace/app.rb' - - 'spec/fixtures/workspace/lib/other.rb' - - 'spec/fixtures/workspace/lib/something.rb' - - 'spec/fixtures/workspace/lib/thing.rb' - - 'spec/fixtures/workspace_folders/folder1/app.rb' - - 'spec/fixtures/workspace_folders/folder2/app.rb' - - 'spec/fixtures/yard_map/attr.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/node_chainer_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/class_variable_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map/node_processor_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/spec_helper.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: @@ -1892,15 +932,7 @@ Style/GlobalStdStream: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/workspace.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSplatArgument. @@ -1935,52 +967,11 @@ Style/IdenticalConditionalBranches: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowIfModifier. Style/IfInsideElse: - Exclude: - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/language_server/transport/data_reader.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/class_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/common.rb' - - 'lib/solargraph/pin/constant.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - - 'lib/solargraph/workspace/config.rb' - - 'lib/solargraph/yard_map/helpers.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -2010,69 +1001,8 @@ Style/MapToSet: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline -Style/MethodDefParentheses: - Exclude: - - 'lib/solargraph.rb' - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/convention.rb' - - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/convention/data_definition/data_assignment_node.rb' - - 'lib/solargraph/convention/data_definition/data_definition_node.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/convention/struct_definition/struct_assignment_node.rb' - - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' - - 'lib/solargraph/diagnostics/rubocop_helpers.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/equality.rb' - - 'lib/solargraph/gem_pins.rb' - - 'lib/solargraph/language_server/host/message_worker.rb' - - 'lib/solargraph/language_server/host/sources.rb' - - 'lib/solargraph/language_server/message/text_document/formatting.rb' - - 'lib/solargraph/location.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - - 'lib/solargraph/parser/node_processor/base.rb' - - 'lib/solargraph/parser/parser_gem/flawed_builder.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/args_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/base_variable.rb' - - 'lib/solargraph/pin/block.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/delegated_method.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/position.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source_map.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/type_checker/checks.rb' - - 'lib/solargraph/yard_map/helpers.rb' - - 'lib/solargraph/yardoc.rb' - - 'spec/doc_map_spec.rb' - - 'spec/fixtures/rdoc-lib/lib/example.rb' - - 'spec/source_map_spec.rb' - - 'spec/spec_helper.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' +Style/MethodDefParentheses: + Enabled: false Style/MultilineBlockChain: Exclude: @@ -2091,29 +1021,13 @@ Style/MultilineTernaryOperator: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMethodComparison, ComparisonsThreshold. Style/MultipleComparison: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: literals, strict Style/MutableConstant: - Exclude: - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/logging.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/rbs_map/core_fills.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/complex_type_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -2148,34 +1062,15 @@ Style/Next: - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. +# Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. Style/NumericLiterals: - Exclude: - - 'lib/solargraph/language_server/error_codes.rb' - - 'spec/language_server/protocol_spec.rb' + MinDigits: 6 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/pin/delegated_method.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/chain/array.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/workspace.rb' + Enabled: false Style/OpenStructUse: Exclude: @@ -2184,14 +1079,7 @@ Style/OpenStructUse: # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/hash.rb' - - 'lib/solargraph/source/chain/z_super.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/updater.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. @@ -2254,14 +1142,7 @@ Style/RedundantInterpolation: # This cop supports safe autocorrection (--autocorrect). Style/RedundantParentheses: - Exclude: - - 'lib/solargraph/diagnostics/type_check.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpArgument: @@ -2274,13 +1155,7 @@ Style/RedundantRegexpArgument: # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/diagnostics/rubocop.rb' - - 'lib/solargraph/language_server/uri_helpers.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleReturnValues. @@ -2294,15 +1169,7 @@ Style/RedundantReturn: # This cop supports safe autocorrection (--autocorrect). Style/RedundantSelf: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/equality.rb' - - 'lib/solargraph/location.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/signature.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/link.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowInnerSlashes. @@ -2323,19 +1190,7 @@ Style/RescueStandardError: # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: - Exclude: - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - - 'lib/solargraph/language_server/request.rb' - - 'lib/solargraph/language_server/transport/data_reader.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/conversions.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/range.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # Configuration parameters: Max. Style/SafeNavigationChainLength: @@ -2344,36 +1199,12 @@ Style/SafeNavigationChainLength: # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/convention/data_definition/data_definition_node.rb' - - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' - - 'lib/solargraph/diagnostics/rubocop_helpers.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain/constant.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker/checks.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. Style/SoleNestedConditional: - Exclude: - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/StderrPuts: @@ -2384,114 +1215,13 @@ Style/StderrPuts: # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'solargraph.gemspec' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: - Exclude: - - 'Gemfile' - - 'Rakefile' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/language_server/message/extended/document_gems.rb' - - 'lib/solargraph/language_server/message/extended/download_core.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/parser_gem/class_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/conversions.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/server_methods.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/workspace.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_tags.rb' - - 'solargraph.gemspec' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/node_chainer_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: @@ -2505,35 +1235,13 @@ Style/SuperArguments: # Configuration parameters: EnforcedStyle, MinSize. # SupportedStyles: percent, brackets Style/SymbolArray: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain/literal.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/source_map/mapper_spec.rb' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. # AllowedMethods: define_method Style/SymbolProc: - Exclude: - - 'lib/solargraph/gem_pins.rb' - - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/language_server/message/text_document/signature_help.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowSafeAssignment. @@ -2546,19 +1254,7 @@ Style/TernaryParentheses: # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/parser/node_processor.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/def_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/defs_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/until_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/while_node.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_map/mapper/to_namespace.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. @@ -2572,13 +1268,7 @@ Style/TrailingCommaInArrayLiteral: # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInHashLiteral: - Exclude: - - 'lib/solargraph/pin/base_variable.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/local_variable.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. @@ -2597,20 +1287,7 @@ Style/WhileUntilModifier: # Configuration parameters: EnforcedStyle, MinSize, WordRegex. # SupportedStyles: percent, brackets Style/WordArray: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/diagnostics/type_check.rb' - - 'lib/solargraph/language_server/message/text_document/formatting.rb' - - 'spec/doc_map_spec.rb' - - 'spec/parser/node_chainer_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). Style/YAMLFileRead: @@ -2619,13 +1296,7 @@ Style/YAMLFileRead: # This cop supports unsafe autocorrection (--autocorrect-all). Style/ZeroLengthPredicate: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/source/chain/array.rb' - - 'spec/language_server/protocol_spec.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -2638,33 +1309,7 @@ YARD/CollectionType: # Configuration parameters: EnforcedStylePrototypeName. # SupportedStylesPrototypeName: before, after YARD/MismatchName: - Exclude: - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/language_server/host/dispatch.rb' - - 'lib/solargraph/language_server/request.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/region.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/base_variable.rb' - - 'lib/solargraph/pin/block.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/closure.rb' - - 'lib/solargraph/pin/delegated_method.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/pin/proxy_type.rb' - - 'lib/solargraph/pin/reference.rb' - - 'lib/solargraph/pin/symbol.rb' - - 'lib/solargraph/pin/until.rb' - - 'lib/solargraph/pin/while.rb' - - 'lib/solargraph/pin_cache.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/z_super.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false YARD/TagTypeSyntax: Exclude: @@ -2673,72 +1318,7 @@ YARD/TagTypeSyntax: - 'lib/solargraph/type_checker.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: - Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/source_to_yard.rb' - - 'lib/solargraph/api_map/store.rb' - - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/convention/data_definition.rb' - - 'lib/solargraph/doc_map.rb' - - 'lib/solargraph/gem_pins.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - - 'lib/solargraph/language_server/message/extended/download_core.rb' - - 'lib/solargraph/language_server/message/initialize.rb' - - 'lib/solargraph/language_server/message/text_document/completion.rb' - - 'lib/solargraph/language_server/message/text_document/definition.rb' - - 'lib/solargraph/language_server/message/text_document/document_highlight.rb' - - 'lib/solargraph/language_server/message/text_document/prepare_rename.rb' - - 'lib/solargraph/language_server/message/text_document/references.rb' - - 'lib/solargraph/language_server/message/text_document/rename.rb' - - 'lib/solargraph/language_server/message/workspace/did_change_watched_files.rb' - - 'lib/solargraph/library.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/and_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/if_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb' - - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin/base.rb' - - 'lib/solargraph/pin/callable.rb' - - 'lib/solargraph/pin/common.rb' - - 'lib/solargraph/pin/documenting.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/rbs_map/conversions.rb' - - 'lib/solargraph/rbs_map/core_fills.rb' - - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source.rb' - - 'lib/solargraph/source/chain.rb' - - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/if.rb' - - 'lib/solargraph/source/chain/instance_variable.rb' - - 'lib/solargraph/source/chain/variable.rb' - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/encoding_fixes.rb' - - 'lib/solargraph/source/source_chainer.rb' - - 'lib/solargraph/source_map.rb' - - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/source_map/mapper.rb' - - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - - 'lib/solargraph/workspace/config.rb' - - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/api_map_spec.rb' - - 'spec/complex_type_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/protocol_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/workspace_spec.rb' + Max: 244 diff --git a/solargraph.gemspec b/solargraph.gemspec index e6bb9394a..7610bb8ea 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -48,9 +48,16 @@ Gem::Specification.new do |s| s.add_development_dependency 'public_suffix', '~> 3.1' s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'rspec', '~> 3.5' - s.add_development_dependency 'rubocop-rake', '~> 0.7' - s.add_development_dependency 'rubocop-rspec', '~> 3.6' - s.add_development_dependency 'rubocop-yard', '~> 1.0' + # + # very specific development-time RuboCop version patterns for CI + # stability - feel free to update in an isolated PR + # + # even more specific on RuboCop itself, which is written into _todo + # file. + s.add_development_dependency 'rubocop', '~> 1.79.2.0' + s.add_development_dependency 'rubocop-rake', '~> 0.7.1' + s.add_development_dependency 'rubocop-rspec', '~> 3.6.0' + s.add_development_dependency 'rubocop-yard', '~> 1.0.0' s.add_development_dependency 'simplecov', '~> 0.21' s.add_development_dependency 'simplecov-lcov', '~> 0.8' s.add_development_dependency 'undercover', '~> 0.7' From 61260f346883de89c8dd9c61b205e88f59ae3a8b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 30 Aug 2025 10:18:50 -0400 Subject: [PATCH 088/524] Fix merge issue --- .github/workflows/linting.yml | 2 +- .rubocop_todo.yml | 25 +++++++++---------------- lib/solargraph/api_map.rb | 2 -- spec/api_map_spec.rb | 3 --- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index aa22ce22c..b4ef26bfe 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -115,7 +115,7 @@ jobs: - name: Run RuboCop against todo file continue-on-error: true run: | - cmd="bundle exec rubocop --auto-gen-config --exclude-limit=5 --no-offense-counts --no-auto-gen-timestampxb*com" + cmd="bundle exec rubocop --auto-gen-config --exclude-limit=5 --no-offense-counts --no-auto-gen-timestamp" ${cmd:?} set +e if [ -n "$(git status --porcelain)" ] diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7641198af..0ed335f34 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,5 +1,5 @@ # This configuration was generated by -# `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# `rubocop --auto-gen-config --exclude-limit 5 --no-offense-counts --no-auto-gen-timestamp` # using RuboCop version 1.80.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. @@ -121,7 +121,13 @@ Layout/EndOfLine: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. - Enabled: false +Layout/ExtraSpacing: + Exclude: + - 'lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb' + - 'lib/solargraph/pin/closure.rb' + - 'lib/solargraph/rbs_map/conversions.rb' + - 'lib/solargraph/type_checker.rb' + - 'spec/spec_helper.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -604,9 +610,9 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' + - 'spec/api_map_method_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/source_map/node_processor_spec.rb' - - 'spec/api_map_method_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. @@ -633,7 +639,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1114,12 +1119,6 @@ Style/RedundantFreeze: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/source_map/mapper.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowComments. -Style/RedundantInitialize: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -1140,12 +1139,6 @@ Style/RedundantRegexpArgument: - 'spec/diagnostics/rubocop_spec.rb' - 'spec/language_server/host_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Enabled: false diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f58633a0c..ee35dc497 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -753,7 +753,6 @@ def store # @param skip [Set] # @param no_core [Boolean] Skip core classes if true # @return [Array] - # rubocop:disable Metrics/CyclomaticComplexity def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false rooted_type = ComplexType.parse(rooted_tag).force_rooted fqns = rooted_type.namespace @@ -827,7 +826,6 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end result end - # rubocop:enable Metrics/CyclomaticComplexity # @param fqns [String] # @param visibility [Array] diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index a612b428e..85e62d507 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -1,7 +1,6 @@ require 'tmpdir' describe Solargraph::ApiMap do - # rubocop:disable RSpec/InstanceVariable before :all do @api_map = Solargraph::ApiMap.new end @@ -873,6 +872,4 @@ def c clip = api_map.clip_at('test.rb', [18, 4]) expect(clip.infer.to_s).to eq('Integer') end - - # rubocop:enable RSpec/InstanceVariable end From f623a73ad88f44d3ff6e6873a1682f33a07d431b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Sep 2025 07:52:39 -0400 Subject: [PATCH 089/524] Pull in overcommit fix --- lib/solargraph/yardoc.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 625e41ce4..ed638a7ce 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -26,7 +26,7 @@ def cache(yard_plugins, gemspec) # # @sg-ignore RBS gem doesn't reflect that Open3.* also include # kwopts from Process.spawn() - stdout_and_stderr_str, status = Open3.capture2e(cmd, chdir: gemspec.gem_dir) + stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } Solargraph.logger.info stdout_and_stderr_str @@ -60,5 +60,22 @@ def load!(gemspec) YARD::Registry.load! PinCache.yardoc_path gemspec YARD::Registry.all end + + # If the BUNDLE_GEMFILE environment variable is set, we need to + # make sure it's an absolute path, as we'll be changing + # directories. + # + # 'bundle exec' sets an absolute path here, but at least the + # overcommit gem does not, breaking on-the-fly documention with a + # spawned yardoc command from our current bundle + # + # @return [Hash{String => String}] a hash of environment variables to override + def current_bundle_env_tweaks + tweaks = {} + if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty? + tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) + end + tweaks + end end end From a8b678b1a1abec6a571ad321b4245575541bf969 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Sep 2025 08:56:57 -0400 Subject: [PATCH 090/524] Add spec --- .rubocop.yml | 3 +++ spec/yardoc_spec.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 spec/yardoc_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..c17a56410 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,9 @@ AllCops: - "vendor/**/.*" TargetRubyVersion: 3.0 +RSpec/SpecFilePathFormat: + Enabled: false + Style/MethodDefParentheses: EnforcedStyle: require_no_parentheses diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb new file mode 100644 index 000000000..34dcad45c --- /dev/null +++ b/spec/yardoc_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'open3' + +describe Solargraph::Yardoc do + let(:gem_yardoc_path) do + Solargraph::PinCache.yardoc_path gemspec + end + + before do + FileUtils.mkdir_p(gem_yardoc_path) + end + + describe '#cache' do + let(:api_map) { Solargraph::ApiMap.new } + let(:doc_map) { api_map.doc_map } + let(:gemspec) { Gem::Specification.find_by_path('rubocop') } + let(:output) { '' } + + before do + allow(Solargraph.logger).to receive(:warn) + allow(Solargraph.logger).to receive(:info) + FileUtils.rm_rf(gem_yardoc_path) + end + + context 'when given a relative BUNDLE_GEMFILE path' do + around do |example| + # turn absolute BUNDLE_GEMFILE path into relative + existing_gemfile = ENV.fetch('BUNDLE_GEMFILE', nil) + current_dir = Dir.pwd + # remove prefix current_dir from path + ENV['BUNDLE_GEMFILE'] = existing_gemfile.sub("#{current_dir}/", '') + raise 'could not figure out relative path' if Pathname.new(ENV.fetch('BUNDLE_GEMFILE', nil)).absolute? + example.run + ENV['BUNDLE_GEMFILE'] = existing_gemfile + end + + it 'sends Open3 an absolute path' do + called_with = nil + allow(Open3).to receive(:capture2e) do |*args| + called_with = args + ['output', instance_double(Process::Status, success?: true)] + end + + described_class.cache([], gemspec) + + expect(called_with[0]['BUNDLE_GEMFILE']).to start_with('/') + end + end + end +end From 9f7fe59e6f172486fc3cce20fad1d8d5ce29bfa6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 2 Sep 2025 05:45:12 -0400 Subject: [PATCH 091/524] Add --references flag (superclass for now) --- lib/solargraph/api_map.rb | 31 ++++++++++++------------- lib/solargraph/shell.rb | 49 +++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 4e8332080..54019d1b8 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -730,6 +730,21 @@ def inner_get_methods_from_reference fq_reference_tag, namespace_pin, type, scop methods end + # @param fq_sub_tag [String] + # @return [String, nil] + def qualify_superclass fq_sub_tag + fq_sub_type = ComplexType.try_parse(fq_sub_tag) + fq_sub_ns = fq_sub_type.name + sup_tag = store.get_superclass(fq_sub_tag) + sup_type = ComplexType.try_parse(sup_tag) + sup_ns = sup_type.name + return nil if sup_tag.nil? + parts = fq_sub_ns.split('::') + last = parts.pop + parts.pop if last == sup_ns + qualify(sup_tag, parts.join('::')) + end + private # A hash of source maps with filename keys. @@ -865,21 +880,6 @@ def qualify_lower namespace, context qualify namespace, context.split('::')[0..-2].join('::') end - # @param fq_sub_tag [String] - # @return [String, nil] - def qualify_superclass fq_sub_tag - fq_sub_type = ComplexType.try_parse(fq_sub_tag) - fq_sub_ns = fq_sub_type.name - sup_tag = store.get_superclass(fq_sub_tag) - sup_type = ComplexType.try_parse(sup_tag) - sup_ns = sup_type.name - return nil if sup_tag.nil? - parts = fq_sub_ns.split('::') - last = parts.pop - parts.pop if last == sup_ns - qualify(sup_tag, parts.join('::')) - end - # @param name [String] Namespace to fully qualify # @param root [String] The context to search # @param skip [Set] Contexts already searched @@ -949,7 +949,6 @@ def resolve_fqns fqns assignment = constant.assignment - # @sg-ignore Wrong argument type for Solargraph::ApiMap#resolve_trivial_constant: node expected AST::Node, received Parser::AST::Node, nil target_ns = resolve_trivial_constant(assignment) if assignment return nil unless target_ns qualify_namespace target_ns, constant_namespace diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 21a53172f..11039b187 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -246,6 +246,8 @@ def list # @sg-ignore Unresolved call to option option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false # @sg-ignore Unresolved call to option + option :references, type: :boolean, desc: 'Show references', default: false + # @sg-ignore Unresolved call to option option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false # @sg-ignore Unresolved call to option option :stack, type: :boolean, desc: 'Show entire stack of a method pin by including definitions in superclasses', default: false @@ -253,27 +255,34 @@ def list # @return [void] def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) - - # @sg-ignore Unresolved call to options - # @type [Array] - pins = if options[:stack] - scope, ns, meth = if path.include? '#' - [:instance, *path.split('#', 2)] - else - [:class, *path.split('.', 2)] - end - - # @sg-ignore Wrong argument type for - # Solargraph::ApiMap#get_method_stack: rooted_tag - # expected String, received Array - api_map.get_method_stack(ns, meth, scope: scope) - else - api_map.get_path_pins path - end - if pins.empty? + pins = api_map.get_path_pins path + references = {} + pin = pins.first + case pin + when nil $stderr.puts "Pin not found for path '#{path}'" exit 1 + when Pin::Method + if options[:stack] + scope, ns, meth = if path.include? '#' + [:instance, *path.split('#', 2)] + else + [:class, *path.split('.', 2)] + end + + # @sg-ignore Wrong argument type for + # Solargraph::ApiMap#get_method_stack: rooted_tag + # expected String, received Array + pins = api_map.get_method_stack(ns, meth, scope: scope) + end + when Pin::Namespace + if options[:references] + superclass_tag = api_map.qualify_superclass(pin.return_type.tag) + superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag + references[:superclass] = superclass_pin if superclass_pin + end end + pins.each do |pin| # @sg-ignore Unresolved call to options if options[:typify] || options[:probe] @@ -288,6 +297,10 @@ def pin path print_pin(pin) end + references.each do |key, refpin| + puts "\n# #{key.to_s.capitalize}:\n\n" + print_pin(refpin) + end end private From 79e8cd900b13c60591e3bc76e3f5bffeb93bbdcb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 2 Sep 2025 05:49:18 -0400 Subject: [PATCH 092/524] Add another @sg-ignore --- lib/solargraph/shell.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index c56be0107..aa7214196 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -278,6 +278,7 @@ def pin path pins = api_map.get_method_stack(ns, meth, scope: scope) end when Pin::Namespace + # @sg-ignore Unresolved call to options if options[:references] superclass_tag = api_map.qualify_superclass(pin.return_type.tag) superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag From da205a3b208c7aad3aedced43b889e70ab247348 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 2 Sep 2025 05:52:39 -0400 Subject: [PATCH 093/524] Catch up with .rubocop_todo.yml --- .rubocop_todo.yml | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c711cd674..c3d28df49 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -920,9 +920,9 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' + - 'spec/api_map_method_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/source_map/node_processor_spec.rb' - - 'spec/api_map_method_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. @@ -1045,7 +1045,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1086,7 +1085,6 @@ RSpec/ImplicitExpect: RSpec/InstanceVariable: Exclude: - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host_spec.rb' @@ -1354,10 +1352,6 @@ RSpec/SpecFilePathFormat: - 'spec/yard_map/mapper/to_method_spec.rb' - 'spec/yard_map/mapper_spec.rb' -RSpec/StubbedMock: - Exclude: - - 'spec/language_server/host/message_worker_spec.rb' - # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -2219,12 +2213,6 @@ Style/RedundantFreeze: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/source_map/mapper.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowComments. -Style/RedundantInitialize: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -2252,24 +2240,13 @@ Style/RedundantRegexpArgument: - 'spec/diagnostics/rubocop_spec.rb' - 'spec/language_server/host_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/diagnostics/rubocop.rb' - 'lib/solargraph/language_server/uri_helpers.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2632,7 +2609,6 @@ YARD/MismatchName: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/language_server/request.rb' From 4c486edec853f702aaed8384379c17c9226d4f0f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 2 Sep 2025 05:58:48 -0400 Subject: [PATCH 094/524] Add another @sg-ignore --- lib/solargraph/shell.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index aa7214196..cb919476c 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -265,6 +265,7 @@ def pin path $stderr.puts "Pin not found for path '#{path}'" exit 1 when Pin::Method + # @sg-ignore Unresolved call to options if options[:stack] scope, ns, meth = if path.include? '#' [:instance, *path.split('#', 2)] From 200e7e42b847bfb554de097d9ba5da6f182a7196 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 2 Sep 2025 07:57:19 -0400 Subject: [PATCH 095/524] Tolerate case statement in specs --- spec/shell_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index e3c85c6e0..b9dc6b327 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -62,6 +62,7 @@ def bundle_exec(*cmd) let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } before do + allow(Solargraph::Pin::Method).to receive(:===).with(to_s_pin).and_return(true) allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) allow(api_map).to receive(:get_path_pins).with('String#to_s').and_return([to_s_pin]) end @@ -105,6 +106,8 @@ def bundle_exec(*cmd) string_new_pin = instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) + allow(Solargraph::Pin::Method).to receive(:===).with(string_new_pin).and_return(true) + allow(api_map).to receive(:get_path_pins).with('String.new').and_return([string_new_pin]) capture_both do shell.options = { stack: true } shell.pin('String.new') @@ -140,6 +143,7 @@ def bundle_exec(*cmd) context 'with no pin' do it 'prints error' do allow(api_map).to receive(:get_path_pins).with('Not#found').and_return([]) + allow(Solargraph::Pin::Method).to receive(:===).with(nil).and_return(false) out = capture_both do shell.options = {} From f63248177198c73cb9474367bdcdd0adce9f0936 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 3 Sep 2025 18:17:14 -0400 Subject: [PATCH 096/524] Centralize all pin-caching logic in PinCache --- .github/workflows/rspec.yml | 4 + .rubocop.yml | 3 + .rubocop_todo.yml | 166 +----- lib/solargraph/api_map.rb | 99 ++-- lib/solargraph/doc_map.rb | 360 ++++--------- lib/solargraph/gem_pins.rb | 13 +- lib/solargraph/library.rb | 41 +- .../parser/parser_gem/class_methods.rb | 3 + lib/solargraph/pin_cache.rb | 508 ++++++++++++++++-- lib/solargraph/rbs_map.rb | 91 +++- lib/solargraph/rbs_map/core_map.rb | 43 +- lib/solargraph/rbs_map/stdlib_map.rb | 25 +- lib/solargraph/shell.rb | 9 +- lib/solargraph/workspace.rb | 88 ++- lib/solargraph/yardoc.rb | 79 ++- spec/doc_map_spec.rb | 149 +++-- spec/gem_pins_spec.rb | 53 +- spec/language_server/host/diagnoser_spec.rb | 3 +- .../host/message_worker_spec.rb | 2 +- spec/library_spec.rb | 312 +++++++---- spec/pin_cache_spec.rb | 197 +++++++ spec/rbs_map_spec.rb | 13 +- spec/shell_spec.rb | 117 +++- spec/spec_helper.rb | 30 +- spec/type_checker/levels/normal_spec.rb | 6 +- spec/yard_map/mapper_spec.rb | 49 +- spec/yardoc_spec.rb | 111 ++++ 27 files changed, 1742 insertions(+), 832 deletions(-) create mode 100644 spec/pin_cache_spec.rb create mode 100644 spec/yardoc_spec.rb diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..7b2255b0e 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,6 +48,8 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version + - name: Install types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: @@ -65,6 +67,8 @@ jobs: bundler-cache: false - name: Install gems run: bundle install + - name: Install types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec - name: Check PR coverage diff --git a/.rubocop.yml b/.rubocop.yml index a73324db2..c17a56410 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,9 @@ AllCops: - "vendor/**/.*" TargetRubyVersion: 3.0 +RSpec/SpecFilePathFormat: + Enabled: false + Style/MethodDefParentheses: EnforcedStyle: require_no_parentheses diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b8d123824..f474d025b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.80.0. +# using RuboCop version 1.80.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -64,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -225,7 +224,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -241,7 +239,6 @@ Layout/IndentationWidth: - 'lib/solargraph/parser/parser_gem/node_processors/block_node.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/namespace.rb' - - 'lib/solargraph/rbs_map.rb' - 'lib/solargraph/shell.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source_map/clip.rb' @@ -295,7 +292,6 @@ Layout/MultilineMethodCallIndentation: # SupportedStyles: aligned, indented Layout/MultilineOperationIndentation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/source.rb' @@ -336,7 +332,6 @@ Layout/SpaceAroundOperators: - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/workspace/config.rb' - 'spec/library_spec.rb' - - 'spec/yard_map/mapper_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. @@ -511,7 +506,6 @@ Lint/DuplicateMethods: - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/signature.rb' - 'lib/solargraph/rbs_map.rb' - - 'lib/solargraph/rbs_map/core_map.rb' - 'lib/solargraph/source/chain/link.rb' # Configuration parameters: AllowComments, AllowEmptyLambdas. @@ -632,7 +626,6 @@ Lint/UnusedMethodArgument: - 'lib/solargraph/convention/base.rb' - 'lib/solargraph/diagnostics/base.rb' - 'lib/solargraph/diagnostics/update_errors.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/pin/namespace.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/source.rb' @@ -652,12 +645,6 @@ Lint/UnusedMethodArgument: - 'lib/solargraph/source/chain/z_super.rb' - 'spec/doc_map_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/solargraph/api_map.rb' - # This cop supports safe autocorrection (--autocorrect). Lint/UselessAssignment: Exclude: @@ -700,7 +687,6 @@ Metrics/AbcSize: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/library.rb' @@ -770,7 +756,6 @@ Metrics/ModuleLength: Exclude: - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin_cache.rb' # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: @@ -817,7 +802,6 @@ Naming/MemoizedInstanceVariableName: - 'lib/solargraph/convention/gemfile.rb' - 'lib/solargraph/convention/gemspec.rb' - 'lib/solargraph/convention/rakefile.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/rbs_map.rb' - 'lib/solargraph/workspace.rb' @@ -871,7 +855,6 @@ Naming/VariableName: RSpec/Be: Exclude: - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - 'spec/source/source_chainer_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -894,7 +877,6 @@ RSpec/BeforeAfterAll: - '**/spec/rails_helper.rb' - '**/spec/support/**/*.rb' - 'spec/api_map_spec.rb' - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' @@ -907,7 +889,6 @@ RSpec/ContextWording: - 'spec/pin/method_spec.rb' - 'spec/pin/parameter_spec.rb' - 'spec/pin/symbol_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - 'spec/type_checker/levels/strict_spec.rb' - 'spec/type_checker/levels/strong_spec.rb' - 'spec/type_checker/levels/typed_spec.rb' @@ -920,9 +901,9 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' + - 'spec/api_map_method_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/source_map/node_processor_spec.rb' - - 'spec/api_map_method_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. @@ -941,7 +922,6 @@ RSpec/DescribedClass: - 'spec/diagnostics/update_errors_spec.rb' - 'spec/diagnostics_spec.rb' - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - 'spec/language_server/host/diagnoser_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host/message_worker_spec.rb' @@ -1045,7 +1025,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1086,7 +1065,6 @@ RSpec/ImplicitExpect: RSpec/InstanceVariable: Exclude: - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host_spec.rb' @@ -1106,11 +1084,6 @@ RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: . -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - EnforcedStyle: receive - RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' @@ -1129,9 +1102,6 @@ RSpec/MultipleExpectations: - 'spec/diagnostics/type_check_spec.rb' - 'spec/diagnostics/update_errors_spec.rb' - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - 'spec/language_server/host_spec.rb' - 'spec/language_server/message/completion_item/resolve_spec.rb' - 'spec/language_server/message/initialize_spec.rb' @@ -1157,7 +1127,6 @@ RSpec/MultipleExpectations: - 'spec/rbs_map/core_map_spec.rb' - 'spec/rbs_map/stdlib_map_spec.rb' - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - 'spec/source/chain/call_spec.rb' - 'spec/source/chain/class_variable_spec.rb' - 'spec/source/chain/global_variable_spec.rb' @@ -1263,106 +1232,6 @@ RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata. -RSpec/SpecFilePathFormat: - Exclude: - - '**/spec/routing/**/*' - - 'spec/api_map/cache_spec.rb' - - 'spec/api_map/config_spec.rb' - - 'spec/api_map/source_to_yard_spec.rb' - - 'spec/api_map/store_spec.rb' - - 'spec/api_map_spec.rb' - - 'spec/convention/activesupport_concern_spec.rb' - - 'spec/convention/struct_definition_spec.rb' - - 'spec/convention_spec.rb' - - 'spec/diagnostics/base_spec.rb' - - 'spec/diagnostics/require_not_found_spec.rb' - - 'spec/diagnostics/rubocop_helpers_spec.rb' - - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/diagnostics/type_check_spec.rb' - - 'spec/diagnostics/update_errors_spec.rb' - - 'spec/diagnostics_spec.rb' - - 'spec/doc_map_spec.rb' - - 'spec/gem_pins_spec.rb' - - 'spec/language_server/host/diagnoser_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/host/message_worker_spec.rb' - - 'spec/language_server/host_spec.rb' - - 'spec/language_server/message/completion_item/resolve_spec.rb' - - 'spec/language_server/message/extended/check_gem_version_spec.rb' - - 'spec/language_server/message/initialize_spec.rb' - - 'spec/language_server/message/text_document/definition_spec.rb' - - 'spec/language_server/message/text_document/formatting_spec.rb' - - 'spec/language_server/message/text_document/hover_spec.rb' - - 'spec/language_server/message/text_document/rename_spec.rb' - - 'spec/language_server/message/text_document/type_definition_spec.rb' - - 'spec/language_server/message/workspace/did_change_configuration_spec.rb' - - 'spec/language_server/message/workspace/did_change_watched_files_spec.rb' - - 'spec/language_server/message_spec.rb' - - 'spec/language_server/transport/adapter_spec.rb' - - 'spec/language_server/transport/data_reader_spec.rb' - - 'spec/language_server/uri_helpers_spec.rb' - - 'spec/library_spec.rb' - - 'spec/logging_spec.rb' - - 'spec/parser/flow_sensitive_typing_spec.rb' - - 'spec/parser/node_methods_spec.rb' - - 'spec/parser/node_processor_spec.rb' - - 'spec/parser_spec.rb' - - 'spec/pin/base_spec.rb' - - 'spec/pin/base_variable_spec.rb' - - 'spec/pin/block_spec.rb' - - 'spec/pin/constant_spec.rb' - - 'spec/pin/delegated_method_spec.rb' - - 'spec/pin/documenting_spec.rb' - - 'spec/pin/instance_variable_spec.rb' - - 'spec/pin/keyword_spec.rb' - - 'spec/pin/local_variable_spec.rb' - - 'spec/pin/method_spec.rb' - - 'spec/pin/namespace_spec.rb' - - 'spec/pin/parameter_spec.rb' - - 'spec/pin/search_spec.rb' - - 'spec/pin/symbol_spec.rb' - - 'spec/position_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - - 'spec/rbs_map/core_map_spec.rb' - - 'spec/rbs_map/stdlib_map_spec.rb' - - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - - 'spec/source/chain/array_spec.rb' - - 'spec/source/chain/call_spec.rb' - - 'spec/source/chain/class_variable_spec.rb' - - 'spec/source/chain/constant_spec.rb' - - 'spec/source/chain/global_variable_spec.rb' - - 'spec/source/chain/head_spec.rb' - - 'spec/source/chain/instance_variable_spec.rb' - - 'spec/source/chain/link_spec.rb' - - 'spec/source/chain/literal_spec.rb' - - 'spec/source/chain/z_super_spec.rb' - - 'spec/source/chain_spec.rb' - - 'spec/source/change_spec.rb' - - 'spec/source/cursor_spec.rb' - - 'spec/source/source_chainer_spec.rb' - - 'spec/source/updater_spec.rb' - - 'spec/source_map/clip_spec.rb' - - 'spec/source_map/mapper_spec.rb' - - 'spec/source_map_spec.rb' - - 'spec/source_spec.rb' - - 'spec/type_checker/checks_spec.rb' - - 'spec/type_checker/levels/normal_spec.rb' - - 'spec/type_checker/levels/strict_spec.rb' - - 'spec/type_checker/levels/strong_spec.rb' - - 'spec/type_checker/levels/typed_spec.rb' - - 'spec/type_checker/rules_spec.rb' - - 'spec/type_checker_spec.rb' - - 'spec/workspace/config_spec.rb' - - 'spec/workspace_spec.rb' - - 'spec/yard_map/mapper/to_method_spec.rb' - - 'spec/yard_map/mapper_spec.rb' - -RSpec/StubbedMock: - Exclude: - - 'spec/language_server/host/message_worker_spec.rb' - # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -1833,7 +1702,6 @@ Style/FrozenStringLiteralComment: - 'spec/rbs_map/core_map_spec.rb' - 'spec/rbs_map/stdlib_map_spec.rb' - 'spec/rbs_map_spec.rb' - - 'spec/shell_spec.rb' - 'spec/source/chain/array_spec.rb' - 'spec/source/chain/call_spec.rb' - 'spec/source/chain/class_variable_spec.rb' @@ -1882,7 +1750,6 @@ Style/GuardClause: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/range.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/source.rb' @@ -1932,16 +1799,13 @@ Style/IfInsideElse: # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/language_server/message/text_document/completion.rb' - 'lib/solargraph/language_server/message/text_document/hover.rb' - - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' @@ -1963,7 +1827,6 @@ Style/IfUnlessModifier: - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' - - 'lib/solargraph/workspace.rb' - 'lib/solargraph/workspace/config.rb' - 'lib/solargraph/yard_map/helpers.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' @@ -2013,7 +1876,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/convention/struct_definition/struct_assignment_node.rb' - 'lib/solargraph/convention/struct_definition/struct_definition_node.rb' - 'lib/solargraph/diagnostics/rubocop_helpers.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/equality.rb' - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host/message_worker.rb' @@ -2050,7 +1912,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/type_checker/checks.rb' - 'lib/solargraph/yard_map/helpers.rb' - - 'lib/solargraph/yardoc.rb' - 'spec/doc_map_spec.rb' - 'spec/fixtures/rdoc-lib/lib/example.rb' - 'spec/source_map_spec.rb' @@ -2225,12 +2086,6 @@ Style/RedundantFreeze: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/source_map/mapper.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowComments. -Style/RedundantInitialize: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -2246,6 +2101,7 @@ Style/RedundantParentheses: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' + - 'lib/solargraph/pin/search.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/type_checker.rb' @@ -2258,24 +2114,13 @@ Style/RedundantRegexpArgument: - 'spec/diagnostics/rubocop_spec.rb' - 'spec/language_server/host_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/diagnostics/rubocop.rb' - 'lib/solargraph/language_server/uri_helpers.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2285,7 +2130,6 @@ Style/RedundantReturn: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type/type_methods.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - 'lib/solargraph/source/chain/z_super.rb' @@ -2322,7 +2166,6 @@ Style/RescueStandardError: Style/SafeNavigation: Exclude: - 'lib/solargraph/api_map/index.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - 'lib/solargraph/language_server/request.rb' - 'lib/solargraph/language_server/transport/data_reader.rb' @@ -2330,7 +2173,6 @@ Style/SafeNavigation: - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/conversions.rb' - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin_cache.rb' - 'lib/solargraph/range.rb' - 'lib/solargraph/type_checker.rb' @@ -2403,7 +2245,6 @@ Style/StringLiterals: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/convention/struct_definition.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/message/extended/document_gems.rb' - 'lib/solargraph/language_server/message/extended/download_core.rb' @@ -2638,7 +2479,6 @@ YARD/MismatchName: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/language_server/request.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f58633a0c..94b2c7fa0 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -29,6 +29,12 @@ def initialize pins: [] index pins end + # @param out [IO, nil] output stream for logging + # @return [void] + def self.reset_core out: nil + @@core_map = RbsMap::CoreMap.new + end + # # This is a mutable object, which is cached in the Chain class - # if you add any fields which change the results of calls (not @@ -46,6 +52,7 @@ def ==(other) self.eql?(other) end + # @return [Integer] def hash equality_fields.hash end @@ -95,11 +102,12 @@ 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_yard_gemspecs&.any? || - @doc_map&.uncached_rbs_collection_gemspecs&.any? || - @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path + workspace.rbs_collection_path != bench.workspace.rbs_collection_path || + @doc_map.any_uncached? + if recreate_docmap - @doc_map = DocMap.new(unresolved_requires, [], bench.workspace) # @todo Implement gem preferences + @doc_map = DocMap.new(unresolved_requires, [], bench.workspace, out: nil) # @todo Implement gem preferences + @gemspecs = @doc_map.workspace.gemspecs @unresolved_requires = @doc_map.unresolved_requires end @cache.clear if store.update(@@core_map.pins, @doc_map.pins, implicit.pins, iced_pins, live_pins) @@ -114,26 +122,14 @@ def catalog bench [self.class, @source_map_hash, implicit, @doc_map, @unresolved_requires] end - # @return [DocMap] - def doc_map - @doc_map ||= DocMap.new([], []) - end + # @return [DocMap, nil] + attr_reader :doc_map # @return [::Array] 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 [Enumerable] def core_pins @@core_map.pins @@ -177,6 +173,7 @@ def clip_at filename, position # Create an ApiMap with a workspace in the specified directory. # # @param directory [String] + # # @return [ApiMap] def self.load directory api_map = new @@ -190,8 +187,8 @@ def self.load directory # @param out [IO, nil] # @return [void] - def cache_all!(out) - @doc_map.cache_all!(out) + def cache_all_for_doc_map! out + @doc_map.cache_doc_map_gems!(out) end # @param gemspec [Gem::Specification] @@ -212,15 +209,16 @@ class << self # # @param directory [String] # @param out [IO] The output stream for messages + # # @return [ApiMap] - def self.load_with_cache directory, out + def self.load_with_cache directory, out = $stderr api_map = load(directory) if api_map.uncached_gemspecs.empty? logger.info { "All gems cached for #{directory}" } return api_map end - api_map.cache_all!(out) + api_map.cache_all_for_doc_map!(out) load(directory) end @@ -236,13 +234,6 @@ def keyword_pins store.pins_by_class(Pin::Keyword) end - # An array of namespace names defined in the ApiMap. - # - # @return [Set] - def namespaces - store.namespaces - end - # True if the namespace exists. # # @param name [String] The namespace to match @@ -326,7 +317,7 @@ def qualify tag, context_tag = '' # @param context_namespace [String] The context namespace in which the # tag was referenced; start from here to resolve the name # @return [String, nil] fully qualified namespace - def qualify_namespace(namespace, context_namespace = '') + def qualify_namespace namespace, context_namespace = '' cached = cache.get_qualified_namespace(namespace, context_namespace) return cached.clone unless cached.nil? result = if namespace.start_with?('::') @@ -356,7 +347,7 @@ def get_includes(fqns) # @param namespace [String] A fully qualified namespace # @param scope [Symbol] :instance or :class # @return [Array] - def get_instance_variable_pins(namespace, scope = :instance) + def get_instance_variable_pins namespace, scope = :instance result = [] used = [namespace] result.concat store.get_instance_variables(namespace, scope) @@ -369,8 +360,10 @@ def get_instance_variable_pins(namespace, scope = :instance) result end - # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins + # @param (see Solargraph::Parser::FlowSensitiveTyping#visible_pins) + # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins + # @return (see Solargraph::Parser::FlowSensitiveTyping#visible_pins) def visible_pins(*args, **kwargs, &blk) Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk) end @@ -379,7 +372,7 @@ def visible_pins(*args, **kwargs, &blk) # # @param namespace [String] A fully qualified namespace # @return [Enumerable] - def get_class_variable_pins(namespace) + def get_class_variable_pins namespace prefer_non_nil_variables(store.get_class_variables(namespace)) end @@ -533,7 +526,8 @@ def get_complex_type_methods complex_type, context = '', internal = false # @param name [String] Method name to look up # @param scope [Symbol] :instance or :class # @param visibility [Array] :public, :protected, and/or :private - # @param preserve_generics [Boolean] + # @param preserve_generics [Boolean] True to preserve any + # unresolved generic parameters, false to erase them # @return [Array] def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false rooted_type = ComplexType.parse(rooted_tag) @@ -559,7 +553,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, # @deprecated Use #get_path_pins instead. # # @param path [String] The path to find - # @return [Enumerable] + # @return [Array] def get_path_suggestions path return [] if path.nil? resolve_method_aliases store.get_path_pins(path) @@ -568,7 +562,7 @@ def get_path_suggestions path # Get an array of pins that match the specified path. # # @param path [String] - # @return [Enumerable] + # @return [Array] def get_path_pins path get_path_suggestions(path) end @@ -658,7 +652,7 @@ def bundled? filename # @param sup [String] The superclass # @param sub [String] The subclass # @return [Boolean] - def super_and_sub?(sup, sub) + def super_and_sub? sup, sub fqsup = qualify(sup) cls = qualify(sub) tested = [] @@ -677,7 +671,7 @@ def super_and_sub?(sup, sub) # @param module_ns [String] The module namespace (no type parameters) # # @return [Boolean] - def type_include?(host_ns, module_ns) + def type_include? host_ns, module_ns store.get_includes(host_ns).map { |inc_tag| ComplexType.parse(inc_tag).name }.include?(module_ns) end @@ -691,10 +685,17 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] 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)}" } + logger.debug do + "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" + end GemPins.combine_method_pins_by_path(with_resolved_aliases) end + # @return [Workspace, nil] + def workspace + @doc_map&.workspace + end + # @param fq_reference_tag [String] A fully qualified whose method should be pulled in # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type # parameter - used to pull generics information @@ -802,7 +803,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, no_core) end else logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" } @@ -812,7 +814,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? - result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, true) + result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, + visibility, true, skip, true) end unless no_core || fqns.empty? type = get_namespace_type(fqns) @@ -846,9 +849,7 @@ def inner_get_constants fqns, visibility, skip result.concat inner_get_constants(qualify(is, fqns), [:public], skip) end fqsc = qualify_superclass(fqns) - unless %w[Object BasicObject].include?(fqsc) - result.concat inner_get_constants(fqsc, [:public], skip) - end + result.concat inner_get_constants(fqsc, [:public], skip) unless %w[Object BasicObject].include?(fqsc) result end @@ -951,8 +952,6 @@ def prefer_non_nil_variables pins include Logging - private - # @param alias_pin [Pin::MethodAlias] # @return [Pin::Method, nil] def resolve_method_alias(alias_pin) @@ -1027,7 +1026,7 @@ def create_resolved_alias_pin(alias_pin, original) # @param rooted_type [ComplexType] # @param pins [Enumerable] # @return [Array] - def erase_generics(namespace_pin, rooted_type, pins) + def erase_generics namespace_pin, rooted_type, pins return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type) logger.debug("Erasing generics on namespace_pin=#{namespace_pin} / rooted_type=#{rooted_type}") @@ -1038,18 +1037,18 @@ def erase_generics(namespace_pin, rooted_type, pins) # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def should_erase_generics_when_done?(namespace_pin, rooted_type) + def should_erase_generics_when_done? namespace_pin, rooted_type has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type) end # @param namespace_pin [Pin::Namespace] - def has_generics?(namespace_pin) + def has_generics? namespace_pin namespace_pin && !namespace_pin.generics.empty? end # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def can_resolve_generics?(namespace_pin, rooted_type) + def can_resolve_generics? namespace_pin, rooted_type has_generics?(namespace_pin) && !rooted_type.all_params.empty? end end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5fe5e03f9..880451592 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -5,122 +5,86 @@ require 'open3' module Solargraph - # A collection of pins generated from required gems. + # A collection of pins generated from specific 'require' statements + # in code. Multiple can be created per workspace, to represent the + # pins available in different files based on their particular + # 'require' lines. # class DocMap include Logging - # @return [Array] - attr_reader :requires - alias required requires + # @return [Workspace] + attr_reader :workspace # @return [Array] attr_reader :preferences - # @return [Array] - attr_reader :pins - - # @return [Array] - def uncached_gemspecs - uncached_yard_gemspecs.concat(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 - - # @return [String, nil] - attr_reader :rbs_collection_path - - # @return [String, nil] - attr_reader :rbs_collection_config_path - - # @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 + # @param workspace [Workspace] + # @param out [IO, nil] output stream for logging + def initialize requires, preferences, workspace, out: $stderr + @provided_requires = requires.compact @preferences = preferences.compact @workspace = workspace - @rbs_collection_path = workspace&.rbs_collection_path - @rbs_collection_config_path = workspace&.rbs_collection_config_path - @environ = Convention.for_global(self) - @requires.concat @environ.requires if @environ - load_serialized_gem_pins - pins.concat @environ.pins + @out = out end - # @param out [IO] - # @return [void] - def cache_all!(out) - # 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.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| - cache(gemspec, out: out) + # @return [Array] + def requires + @requires ||= @provided_requires + (workspace.global_environ&.requires || []) + end + alias required requires + + # @return [Array] + def uncached_gemspecs + if @uncached_gemspecs.nil? + @uncached_gemspecs = [] + pins # force lazy-loaded pin lookup end - load_serialized_gem_pins - @uncached_rbs_collection_gemspecs = [] - @uncached_yard_gemspecs = [] + @uncached_gemspecs end - # @param gemspec [Gem::Specification] - # @param out [IO] - # @return [void] - def cache_yard_pins(gemspec, out) - pins = GemPins.build_yard_pins(yard_plugins, gemspec) - PinCache.serialize_yard_gem(gemspec, pins) - logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + # @return [Array] + def pins + @pins ||= load_serialized_gem_pins + (workspace.global_environ&.pins || []) end - # @param gemspec [Gem::Specification] - # @param out [IO] # @return [void] - 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) - 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? } + def reset_pins! + @uncached_gemspecs = nil + @pins = nil end - # @param gemspec [Gem::Specification] - # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= workspace.fresh_pincache + end + + def any_uncached? + uncached_gemspecs.any? + end + + # Cache all pins needed for the sources in this doc_map # @param out [IO, nil] output stream for logging # @return [void] - def cache(gemspec, rebuild: false, out: nil) - 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 + def cache_doc_map_gems! out + unless uncached_gemspecs.empty? + logger.info do + gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') + "Caching pins for gems: #{gem_desc}" + end end - cache_yard_pins(gemspec, out) if build_yard - cache_rbs_collection_pins(gemspec, out) if build_rbs_collection - end - - # @return [Array] - def gemspecs - @gemspecs ||= required_gems_map.values.compact.flatten + time = Benchmark.measure do + uncached_gemspecs.each do |gemspec| + cache(gemspec, out: out) + end + end + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any? + out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms" + end + reset_pins! end # @return [Array] @@ -128,77 +92,81 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def self.all_yard_gems_in_memory - @yard_gems_in_memory ||= {} - end - - # @return [Hash{String => Hash{Array(String, String) => Array}}] stored by RBS collection path - def self.all_rbs_collection_gems_in_memory - @rbs_collection_gems_in_memory ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def yard_pins_in_memory - self.class.all_yard_gems_in_memory - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def rbs_collection_pins_in_memory - self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {} - end - - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def self.all_combined_pins_in_memory - @combined_pins_in_memory ||= {} + # @return [Set] + # @param out [IO] + def dependencies out: $stderr + @dependencies ||= + begin + all_deps = gemspecs.flat_map { |spec| fetch_dependencies(spec, out: out) } + existing_gems = gemspecs.map(&:name) + all_deps.reject { |gemspec| existing_gems.include? gemspec.name }.to_set + end end - # @todo this should also include an index by the hash of the RBS collection - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version - def combined_pins_in_memory - self.class.all_combined_pins_in_memory + # Cache gem documentation if needed for this doc_map + # + # @param gemspec [Gem::Specification] + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @param out [IO, nil] output stream for logging + # + # @return [void] + def cache gemspec, rebuild: false, out: nil + pin_cache.cache_gem(gemspec: gemspec, + rebuild: rebuild, + out: out) end - # @return [Array] - def yard_plugins - @environ.yard_plugins - end + private - # @return [Set] - def dependencies - @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set + # @return [Array] + def gemspecs + @gemspecs ||= required_gems_map.values.compact.flatten end - private - - # @return [void] - def load_serialized_gem_pins - @pins = [] - @uncached_yard_gemspecs = [] - @uncached_rbs_collection_gemspecs = [] + # @param out [IO, nil] + # @return [Array] + def load_serialized_gem_pins out: @out + serialized_pins = [] with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v } - # @sg-ignore Wrong argument type for Hash.[]: arg_0 expected _ToHash), undefined>, received Array)> + # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] - paths = Hash[without_gemspecs].keys - # @sg-ignore Wrong argument type for Hash.[]: arg_0 expected _ToHash), undefined>, received Array)> + missing_paths = Hash[without_gemspecs].keys + # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] - gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a - - paths.each do |path| - rbs_pins = deserialize_stdlib_rbs_map path + gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies(out: out).to_a + + missing_paths.each do |path| + # this will load from disk if needed; no need to manage + # uncached_gemspecs to trigger that later + stdlib_name_guess = path.split('/').first + + # try to resolve the stdlib name + deps = workspace.stdlib_dependencies(stdlib_name_guess) || [] + [stdlib_name_guess, *deps].compact.each do |potential_stdlib_name| + rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name + serialized_pins.concat rbs_pins if rbs_pins + end end - logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." } + existing_pin_count = serialized_pins.length time = Benchmark.measure do gemspecs.each do |gemspec| - pins = deserialize_combined_pin_cache gemspec - @pins.concat pins if pins + # only deserializes already-cached gems + gemspec_pins = pin_cache.deserialize_combined_pin_cache gemspec + if gemspec_pins + serialized_pins.concat gemspec_pins + else + uncached_gemspecs << gemspec + end end end - 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 + pins_processed = serialized_pins.length - existing_pin_count + milliseconds = (time.real * 1000).round + if (milliseconds > 500) && out && gemspecs.any? + out.puts "Deserialized #{serialized_pins.length} gem pins from #{PinCache.base_dir} in #{milliseconds} ms" + end + uncached_gemspecs.uniq! { |gemspec| "#{gemspec.name}:#{gemspec.version}" } + serialized_pins end # @return [Hash{String => Array}] @@ -211,102 +179,6 @@ 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 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 { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" } - return nil - end - end - - # @param path [String] require path that might be in the RBS stdlib collection - # @return [void] - 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.debug { "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 in RBS" } unless path == 'set' - nil - end - end - - # @param gemspec [Gem::Specification] - # @param rbs_version_cache_key [String] - # @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 - end - # @param path [String] # @return [::Array, nil] def resolve_path_to_gemspecs path @@ -354,8 +226,10 @@ def change_gemspec_version gemspec, version end # @param gemspec [Gem::Specification] + # @param out [IO, nil] + # # @return [Array] - def fetch_dependencies gemspec + def fetch_dependencies gemspec, out: nil # @param spec [Gem::Dependency] only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index a193a8a39..3b61b6a65 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -41,15 +41,6 @@ def self.combine_method_pins(*pins) out end - # @param yard_plugins [Array] The names of YARD plugins to use. - # @param gemspec [Gem::Specification] - # @return [Array] - def self.build_yard_pins(yard_plugins, gemspec) - Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec) - yardoc = Yardoc.load!(gemspec) - YardMap::Mapper.new(yardoc, gemspec).map - end - # @param yard_pins [Array] # @param rbs_pins [Array] # @@ -68,7 +59,9 @@ def self.combine(yard_pins, rbs_pins) end 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}" } + logger.debug do + "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" + end out end in_rbs_only = rbs_pins.select do |pin| diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 9d5162431..9b4072d69 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -1,9 +1,16 @@ # frozen_string_literal: true +require 'rubygems' require 'pathname' require 'observer' require 'open3' +# @!parse +# class ::Gem::Specification +# # @return [String] +# def name; end +# end + module Solargraph # A Library handles coordination between a Workspace and an ApiMap. # @@ -273,12 +280,12 @@ def references_from filename, line, column, strip: false, only: false # HACK: for language clients that exclude special characters from the start of variable names if strip && match = cursor.word.match(/^[^a-z0-9_]+/i) found.map! do |loc| - Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column)) + Solargraph::Location.new(loc.filename, + Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, + loc.range.ending.column)) end end - result.concat(found.sort do |a, b| - a.range.start.line <=> b.range.start.line - end) + result.concat(found.sort { |a, b| a.range.start.line <=> b.range.start.line }) end result.uniq end @@ -303,9 +310,7 @@ def locate_ref location return nil if pin.nil? # @param full [String] return_if_match = proc do |full| - if source_map_hash.key?(full) - return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) - end + return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) if source_map_hash.key?(full) end workspace.require_paths.each do |path| full = File.join path, pin.name @@ -500,7 +505,12 @@ def external_requires private - # @return [Hash{String => Array}] + # @return [PinCache] + def pin_cache + workspace.pin_cache + end + + # @return [Hash{String => Set}] def source_map_external_require_hash @source_map_external_require_hash ||= {} end @@ -580,12 +590,13 @@ def cache_errors def cache_next_gemspec return if @cache_progress + # @type [Gem::Specification] spec = cacheable_specs.first return end_cache_progress unless spec pending = api_map.uncached_gemspecs.length - cache_errors.length - 1 - if Yardoc.processing?(spec) + if pin_cache.yardoc_processing?(spec) logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)" queued_gemspec_cache.push(spec) return if pending - queued_gemspec_cache.length < 1 @@ -596,7 +607,11 @@ def cache_next_gemspec logger.info "Caching #{spec.name} #{spec.version}" Thread.new do report_cache_progress spec.name, pending - _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s) + kwargs = {} + kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty? + # @sg-ignore Unresolved call to capture3 on Module + _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s, + **kwargs) if s.success? logger.info "Cached #{spec.name} #{spec.version}" else @@ -613,8 +628,7 @@ def cache_next_gemspec # @return [Array] def cacheable_specs - cacheable = api_map.uncached_yard_gemspecs + - api_map.uncached_rbs_collection_gemspecs - + cacheable = api_map.uncached_gemspecs + queued_gemspec_cache - cache_errors.to_a return cacheable unless cacheable.empty? @@ -673,8 +687,7 @@ def sync_catalog source_map_hash.values.each { |map| find_external_requires(map) } api_map.catalog bench logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" - logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" - logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" + logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs" cache_next_gemspec @sync_count = 0 end diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index ddc742bd8..50c537531 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -32,6 +32,9 @@ def parse code, filename = nil, line = 0 buffer = ::Parser::Source::Buffer.new(filename, line) buffer.source = code parser.parse(buffer) + # @sg-ignore Unresolved type Parser::SyntaxError, + # Parser::UnknownEncodingInMagicComment for variable e + # https://github.com/castwide/solargraph/pull/1005 rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e raise Parser::SyntaxError, e.message end diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 2a0ec4639..8c47cfef4 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -1,12 +1,424 @@ -require 'yard-activesupport-concern' require 'fileutils' require 'rbs' +require 'rubygems' module Solargraph - module PinCache + class PinCache + include Logging + + attr_reader :directory, :rbs_collection_path, :rbs_collection_config_path, :yard_plugins + + # @param rbs_collection_path [String, nil] + # @param rbs_collection_config_path [String, nil] + # @param directory [String, nil] + # @param yard_plugins [Array] + def initialize rbs_collection_path:, rbs_collection_config_path:, + directory:, + yard_plugins: + @rbs_collection_path = rbs_collection_path + @rbs_collection_config_path = rbs_collection_config_path + @directory = directory + @yard_plugins = yard_plugins + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def cached? gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + combined_gem?(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param out [IO, nil] output stream for logging + # @return [void] + def cache_gem gemspec:, rebuild: false, out: nil + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + build_yard, build_rbs_collection, build_combined = + calculate_build_needs(gemspec, + rebuild: rebuild, + rbs_version_cache_key: rbs_version_cache_key) + + return unless build_yard || build_rbs_collection || build_combined + + build_combine_and_cache(gemspec, + rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String] + def suppress_yard_cache? gemspec, rbs_version_cache_key + if gemspec.name == 'parser' && rbs_version_cache_key != RbsMap::CACHE_KEY_UNRESOLVED + # parser takes forever to build YARD pins, but has excellent RBS collection pins + return true + end + false + end + + # @param out [IO, nil] output stream for logging + # + # @return [void] + def cache_all_stdlibs out: $stderr + possible_stdlibs.each do |stdlib| + RbsMap::StdlibMap.new(stdlib, out: out) + end + end + + # @param path [String] require path that might be in the RBS stdlib collection + # @return [void] + def cache_stdlib_rbs_map path + # these are held in memory in RbsMap::StdlibMap + map = RbsMap::StdlibMap.load(path) + if map.resolved? + logger.debug { "Loading stdlib pins for #{path}" } + pins = map.pins + logger.debug { "Loaded #{pins.length} stdlib pins for #{path}" } + pins + else + # @todo Temporarily ignoring unresolved `require 'set'` + logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set' + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # + # @return [String] + def lookup_rbs_version_cache_key gemspec + rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) + rbs_map.cache_key + end + + # @param gemspec [Gem::Specification] + # @param rbs_version_cache_key [String] + # @param yard_pins [Array] + # @param rbs_collection_pins [Array] + # @return [void] + def cache_combined_pins gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins + combined_pins = GemPins.combine(yard_pins, rbs_collection_pins) + serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def deserialize_combined_pin_cache gemspec + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + + load_combined_gem(gemspec, rbs_version_cache_key) + end + + # @param gemspec [Gem::Specification] + # @param out [IO, nil] + # @return [void] + def uncache_gem gemspec, out: nil + PinCache.uncache(yardoc_path(gemspec), out: out) + PinCache.uncache(yard_gem_path(gemspec), out: out) + uncache_by_prefix(rbs_collection_pins_path_prefix(gemspec), out: out) + uncache_by_prefix(combined_path_prefix(gemspec), out: out) + combined_pins_in_memory.delete([gemspec.name, gemspec.version]) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + def yardoc_processing? gemspec + Yardoc.processing?(yardoc_path(gemspec)) + end + + # @return [Array] a list of possible standard library names + def possible_stdlibs + # all dirs and .rb files in Gem::RUBYGEMS_DIR + Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| + basename = File.basename(file_or_dir) + # remove .rb + basename = basename[0..-4] if basename.end_with?('.rb') + basename + end.sort.uniq + rescue StandardError => e + logger.info { "Failed to get possible stdlibs: #{e.message}" } + logger.debug { e.backtrace.join("\n") } + [] + end + + private + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists + # @param rbs_version_cache_key [String, nil] the cache key for the gem in the RBS collection + # + # @return [Array(Boolean, Boolean, Boolean)] whether to build YARD + # pins, RBS collection pins, and combined pins + def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key: + if rebuild + build_yard = true + build_rbs_collection = true + build_combined = true + else + build_yard = !yard_gem?(gemspec) + build_rbs_collection = !rbs_collection_pins?(gemspec, rbs_version_cache_key) + build_combined = !combined_gem?(gemspec, rbs_version_cache_key) || build_yard || build_rbs_collection + end + + build_yard = false if suppress_yard_cache?(gemspec, rbs_version_cache_key) + + [build_yard, build_rbs_collection, build_combined] + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [IO, nil] + # + # @return [void] + def build_combine_and_cache gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + log_cache_info(gemspec, rbs_version_cache_key, + build_yard: build_yard, + build_rbs_collection: build_rbs_collection, + build_combined: build_combined, + out: out) + cache_yard_pins(gemspec, out) if build_yard + # this can be nil even if we aren't told to build it - see suppress_yard_cache? + yard_pins = deserialize_yard_pin_cache(gemspec) || [] + cache_rbs_collection_pins(gemspec, out) if build_rbs_collection + rbs_collection_pins = deserialize_rbs_collection_cache(gemspec, rbs_version_cache_key) || [] + cache_combined_pins(gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins) if build_combined + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] + # @param build_yard [Boolean] + # @param build_rbs_collection [Boolean] + # @param build_combined [Boolean] + # @param out [IO, nil] + # + # @return [void] + def log_cache_info gemspec, + rbs_version_cache_key, + build_yard:, + build_rbs_collection:, + build_combined:, + out: + type = [] + type << 'YARD' if build_yard + rbs_source_desc = RbsMap.rbs_source_desc(rbs_version_cache_key) + type << rbs_source_desc if build_rbs_collection && !rbs_source_desc.nil? + # we'll build it anyway, but it won't take long to build with + # only a single source + + # 'combining' is awkward terminology in this case + just_yard = build_yard && rbs_source_desc.nil? + + type << 'combined' if build_combined && !just_yard + out&.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] + # @return [Array] + def cache_yard_pins gemspec, out + gem_yardoc_path = yardoc_path(gemspec) + Yardoc.build_docs(gem_yardoc_path, yard_plugins, gemspec) unless Yardoc.docs_built?(gem_yardoc_path) + pins = Yardoc.build_pins(gem_yardoc_path, gemspec, out: out) + serialize_yard_gem(gemspec, pins) + logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty? + pins + end + + # @return [Hash{Array(String, String, String) => Array}] + def combined_pins_in_memory + PinCache.all_combined_pins_in_memory[yard_plugins] ||= {} + end + + # @param gemspec [Gem::Specification] + # @param _out [IO, nil] + # @return [Array] + 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 ||= [] + serialize_rbs_collection_pins(gemspec, rbs_version_cache_key, pins) + logger.info do + unless pins.empty? + "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with " \ + "cache_key #{rbs_version_cache_key.inspect}" + end + end + pins + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def deserialize_yard_pin_cache gemspec + cached = load_yard_gem(gemspec) + if cached + cached + else + logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}" + nil + end + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String] + # @return [Array] + def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key + cached = load_rbs_collection_pins(gemspec, rbs_version_cache_key) + Solargraph.assert_or_log(:pin_cache_rbs_collection, 'Asked for non-existent rbs collection') if cached.nil? + logger.info do + "Loaded #{cached&.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" + end + cached + end + + # @return [Array] + def yard_path_components + ["yard-#{YARD::VERSION}", + yard_plugins.sort.uniq.join('-')] + end + + # @param gemspec [Gem::Specification] + # @return [String] + def yardoc_path gemspec + File.join(PinCache.base_dir, + *yard_path_components, + "#{gemspec.name}-#{gemspec.version}.yardoc") + end + + # @param gemspec [Gem::Specification] + # @return [String] + def yard_gem_path gemspec + File.join(PinCache.work_dir, *yard_path_components, "#{gemspec.name}-#{gemspec.version}.ser") + end + + # @param gemspec [Gem::Specification] + # @return [Array, nil] + def load_yard_gem gemspec + PinCache.load(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification] + # @param pins [Array] + # @return [void] + def serialize_yard_gem gemspec, pins + PinCache.save(yard_gem_path(gemspec), pins) + end + + # @param gemspec [Gem::Specification] + # @return [Boolean] + def yard_gem? gemspec + exist?(yard_gem_path(gemspec)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [String] + def rbs_collection_pins_path gemspec, hash + rbs_collection_pins_path_prefix(gemspec) + "#{hash || 0}.ser" + end + + # @param gemspec [Gem::Specification] + # @return [String] + def rbs_collection_pins_path_prefix gemspec + File.join(PinCache.work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String] + # + # @return [Array, nil] + def load_rbs_collection_pins gemspec, hash + PinCache.load(rbs_collection_pins_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_rbs_collection_pins gemspec, hash, pins + PinCache.save(rbs_collection_pins_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [String] + def combined_path gemspec, hash + File.join(combined_path_prefix(gemspec) + "-#{hash || 0}.ser") + end + + # @param gemspec [Gem::Specification] + # @return [String] + def combined_path_prefix gemspec + File.join(PinCache.work_dir, 'combined', yard_plugins.sort.join('-'), "#{gemspec.name}-#{gemspec.version}") + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @param pins [Array] + # @return [void] + def serialize_combined_gem gemspec, hash, pins + PinCache.save(combined_path(gemspec, hash), pins) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String] + def combined_gem? gemspec, hash + exist?(combined_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String, nil] + # @return [Array, nil] + def load_combined_gem gemspec, hash + PinCache.load(combined_path(gemspec, hash)) + end + + # @param gemspec [Gem::Specification] + # @param hash [String] + def rbs_collection_pins? gemspec, hash + exist?(rbs_collection_pins_path(gemspec, hash)) + end + + include Logging + + # @param path [String] + def exist? *path + File.file? File.join(*path) + 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}" + Dir.glob(glob).each do |file| + next unless File.file?(file) + FileUtils.rm_rf file, secure: true + out&.puts "Clearing pin cache in #{file}" + end + end + class << self include Logging + # @return [Hash{Array => Hash{Array(String, String) => + # Array}}] yard plugins, then gemspec name and + # version + def all_combined_pins_in_memory + @all_combined_pins_in_memory ||= {} + end + # The base directory where cached YARD documentation and serialized pins are serialized # # @return [String] @@ -18,6 +430,32 @@ def base_dir File.join(Dir.home, '.cache', 'solargraph') end + # @param path_segments [Array] + # @return [void] + 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}" + else + out&.puts "Pin cache file #{path} does not exist" + end + end + + # @param out [IO, nil] + # @return [void] + def uncache_core out: nil + uncache(core_path, out: out) + # ApiMap keep this in memory + ApiMap.reset_core(out: out) + end + + # @param out [IO, nil] + # @return [void] + def uncache_stdlib out: nil + uncache(stdlib_path, out: out) + end + # The working directory for the current Ruby, RBS, and Solargraph versions. # # @return [String] @@ -27,15 +465,6 @@ def work_dir File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") end - # @param gemspec [Gem::Specification] - # @return [String] - def yardoc_path gemspec - File.join(base_dir, - "yard-#{YARD::VERSION}", - "yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}", - "#{gemspec.name}-#{gemspec.version}.yardoc") - end - # @return [String] def stdlib_path File.join(work_dir, 'stdlib') @@ -164,33 +593,11 @@ def has_rbs_collection?(gemspec, hash) exist?(rbs_collection_path(gemspec, hash)) end - # @return [void] - def uncache_core - uncache(core_path) - end - - # @return [void] - def uncache_stdlib - uncache(stdlib_path) - end - - # @param gemspec [Gem::Specification] - # @param out [IO, nil] - # @return [void] - 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] def clear FileUtils.rm_rf base_dir, secure: true end - private - # @param file [String] # @return [Array, nil] def load file @@ -202,11 +609,6 @@ def load file nil end - # @param path [String] - def exist? *path - File.file? File.join(*path) - end - # @param file [String] # @param pins [Array] # @return [void] @@ -218,27 +620,19 @@ def save file, pins logger.debug { "Cache#save: Saved #{pins.length} pins to #{file}" } end - # @param path_segments [Array] - # @return [void] - 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 + def core? + File.file?(core_path) 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 + # @param out [IO, nil] + # @return [Array] + def cache_core out: $stderr + RbsMap::CoreMap.new.cache_core(out: out) + end + + # @param path [String] + def exist? *path + File.file? File.join(*path) end end end diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index c6c10bac6..f6309bb55 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -23,10 +23,11 @@ class RbsMap attr_reader :rbs_collection_config_path # @param library [String] - # @param version [String, nil + # @param version [String, nil] # @param rbs_collection_config_path [String, Pathname, nil] # @param rbs_collection_paths [Array] - def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [] + # @param out [IO, nil] where to log messages + def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [], out: $stderr if rbs_collection_config_path.nil? && !rbs_collection_paths.empty? raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths' end @@ -37,6 +38,28 @@ def initialize library, version = nil, rbs_collection_config_path: nil, rbs_coll add_library loader, library, version end + CACHE_KEY_GEM_EXPORT = 'gem-export' + CACHE_KEY_UNRESOLVED = 'unresolved' + CACHE_KEY_STDLIB = 'stdlib' + CACHE_KEY_LOCAL = 'local' + + # @param cache_key [String] + # @return [String, nil] a description of the source of the RBS info + def self.rbs_source_desc cache_key + case cache_key + when CACHE_KEY_GEM_EXPORT + 'RBS gem export' + when CACHE_KEY_UNRESOLVED + nil + when CACHE_KEY_STDLIB + 'RBS standard library' + when CACHE_KEY_LOCAL + 'local RBS shims' + else + 'RBS collection' + end + end + # @return [RBS::EnvironmentLoader] def loader @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository) @@ -47,9 +70,13 @@ def loader # updated upstream for the same library and version. May change # if the config for where information comes form changes. def cache_key + return CACHE_KEY_UNRESOLVED unless resolved? + @hextdigest ||= begin # @type [String, nil] data = nil + # @type gem_config [nil, Hash{String => Hash{String => String}}] + gem_config = nil if rbs_collection_config_path lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path)) if lockfile_path.exist? @@ -58,21 +85,26 @@ def cache_key data = gem_config&.to_s end end - if data.nil? || data.empty? - if resolved? - # definitely came from the gem itself and not elsewhere - - # only one version per gem - 'gem-export' + if gem_config.nil? + CACHE_KEY_STDLIB + else + # @type [String] + source = gem_config.dig('source', 'type') + case source + when 'rubygems' + CACHE_KEY_GEM_EXPORT + when 'local' + CACHE_KEY_LOCAL + when 'stdlib' + CACHE_KEY_STDLIB else - 'unresolved' + Digest::SHA1.hexdigest(data) end - else - Digest::SHA1.hexdigest(data) end end end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param rbs_collection_path [String, Pathname, nil] # @param rbs_collection_config_path [String, Pathname, nil] # @return [RbsMap] @@ -83,14 +115,24 @@ def self.from_gemspec gemspec, rbs_collection_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) + rbs_map = RbsMap.new(gemspec.name, nil, + rbs_collection_paths: [rbs_collection_path].compact, + rbs_collection_config_path: rbs_collection_config_path) + + return rbs_map if rbs_map.resolved? + + StdlibMap.new(gemspec.name) end + # @param out [IO, nil] where to log messages # @return [Array] - def pins - @pins ||= resolved? ? conversions.pins : [] + def pins out: $stderr + @pins ||= if resolved? + loader.libs.each { |lib| log_caching(lib, out: out) } + conversions.pins + else + [] + end end # @generic T @@ -140,15 +182,22 @@ def conversions @conversions ||= Conversions.new(loader: loader) end + # @param lib [RBS::EnvironmentLoader::Library] + # @param out [IO, nil] where to log messages + # @return [void] + def log_caching lib, out:; end + # @param loader [RBS::EnvironmentLoader] # @param library [String] - # @param version [String, nil] + # @param version [String, nil] the version of the library to load, or nil for any + # @param out [IO, nil] where to log messages # @return [Boolean] true if adding the library succeeded - def add_library loader, library, version + def add_library loader, library, version, out: $stderr @resolved = if loader.has_library?(library: library, version: version) - loader.add library: library, version: version - logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } - true + # we find our own dependencies from gemfile.lock + loader.add library: library, version: version, resolve_dependencies: false + logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } + true else logger.info { "#{short_name} did not find data for library #{library}:#{version}" } false diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 8c3d7dbdd..a9b647b20 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -14,28 +14,37 @@ def resolved? def initialize; end + # @param out [IO, nil] output stream for logging # @return [Enumerable] - def pins + def pins out: $stderr return @pins if @pins + @pins = cache_core(out: out) + end - @pins = [] + # @param out [IO, nil] output stream for logging + # @return [Array] + def cache_core out: $stderr + new_pins = [] cache = PinCache.deserialize_core - if cache - @pins.replace cache - else - loader.add(path: Pathname(FILLS_DIRECTORY)) - @pins = conversions.pins - @pins.concat RbsMap::CoreFills::ALL - processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } - @pins.replace processed - - PinCache.serialize_core @pins - end - @pins - end + return cache if cache - def loader - @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) + new_pins.concat conversions.pins + + # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader + fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false)) + fill_loader.add(path: Pathname(FILLS_DIRECTORY)) + out&.puts 'Caching RBS pins for Ruby core' + fill_conversions = Conversions.new(loader: fill_loader) + new_pins.concat fill_conversions.pins + + new_pins.concat RbsMap::CoreFills::ALL + + processed = ApiMap::Store.new(new_pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) } + new_pins.replace processed + + PinCache.serialize_core new_pins + + new_pins end private diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index b6804157f..e7891bfe3 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -12,8 +12,13 @@ class StdlibMap < RbsMap # @type [Hash{String => RbsMap}] @stdlib_maps_hash = {} + def log_caching lib, out: $stderr + out&.puts("Caching RBS pins for standard library #{lib.name}") + end + # @param library [String] - def initialize library + # @param out [IO, nil] where to log messages + def initialize library, out: $stderr cached_pins = PinCache.deserialize_stdlib_require library if cached_pins @pins = cached_pins @@ -24,7 +29,7 @@ def initialize library super unless resolved? @pins = [] - logger.info { "Could not resolve #{library.inspect}" } + logger.debug { "StdlibMap could not resolve #{library.inspect}" } return end generated_pins = pins @@ -33,6 +38,22 @@ def initialize library end end + # @return [RBS::Collection::Sources::Stdlib] + def self.source + @source ||= RBS::Collection::Sources::Stdlib.instance + end + + # @param name [String] + # @param version [String, nil] + # @return [Array String}>, nil] + def self.stdlib_dependencies name, version = nil + if source.has?(name, version) + source.dependencies_of(name, version) + else + [] + end + end + # @param library [String] # @return [StdlibMap] def self.load library diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..735e6c290 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -3,6 +3,7 @@ require 'benchmark' require 'thor' require 'yard' +require 'yaml' module Solargraph class Shell < Thor @@ -118,19 +119,21 @@ def cache gem, version = nil # @return [void] def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? + workspace = Solargraph::Workspace.new(Dir.pwd) + gems.each do |gem| if gem == 'core' - PinCache.uncache_core + PinCache.uncache_core(out: $stdout) next end if gem == 'stdlib' - PinCache.uncache_stdlib + PinCache.uncache_stdlib(out: $stdout) next end spec = Gem::Specification.find_by_name(gem) - PinCache.uncache_gem(spec, out: $stdout) + workspace.uncache_gem(spec, out: $stdout) end end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index ffd653d96..92be0c8c3 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -2,6 +2,7 @@ require 'open3' require 'json' +require 'yaml' module Solargraph # A workspace consists of the files in a project's directory and the @@ -9,7 +10,10 @@ module Solargraph # in an associated Library or ApiMap. # class Workspace + include Logging + autoload :Config, 'solargraph/workspace/config' + autoload :RequirePaths, 'solargraph/workspace/require_paths' # @return [String] attr_reader :directory @@ -19,14 +23,12 @@ class Workspace # @return [Array] attr_reader :require_paths - # @return [Array] - attr_reader :gemnames - alias source_gems gemnames - - # @param directory [String] + # @param directory [String] TODO: Document and test '' and '*' semantics # @param config [Config, nil] # @param server [Hash] def initialize directory = '', config = nil, server = {} + raise ArgumentError, 'directory must be a String' unless directory.is_a?(String) + @directory = directory @config = config @server = server @@ -41,6 +43,56 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @return [Solargraph::PinCache] + def pin_cache + @pin_cache ||= fresh_pincache + end + + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || [] + deps.map { |dep| dep['name'] }.compact + end + + # @return [Environ] + def global_environ + # empty docmap, since the result needs to work in any possible + # context here + @global_environ ||= Convention.for_global(DocMap.new([], [], self)) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # + # @return [void] + def cache_gem gemspec, out: nil, rebuild: false + pin_cache.cache_gem(gemspec: gemspec, out: out, rebuild: rebuild) + end + + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [IO, nil] output stream for logging + # + # @return [void] + def uncache_gem gemspec, out: nil + pin_cache.uncache_gem(gemspec, out: out) + end + + # @return [Solargraph::PinCache] + def fresh_pincache + PinCache.new(rbs_collection_path: rbs_collection_path, + rbs_collection_config_path: rbs_collection_config_path, + yard_plugins: yard_plugins, + directory: directory) + end + + # @return [Array] + def yard_plugins + @yard_plugins ||= global_environ.yard_plugins.sort.uniq + end + # Merge the source. A merge will update the existing source for the file # or add it to the sources if the workspace is configured to include it. # The source is ignored if the configuration excludes it. @@ -135,12 +187,23 @@ def rbs_collection_path # @return [String, nil] 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) + @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 + end + + # @param name [String] + # @param version [String, nil] + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError + nil end # Synchronize the workspace from the provided updater. @@ -182,7 +245,10 @@ def load_sources source_hash.clear unless directory.empty? || directory == '*' size = config.calculated.length - raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files + if config.max_files > 0 and size > config.max_files + raise WorkspaceTooLargeError, + "The workspace is too large to index (#{size} files, #{config.max_files} max)" + end config.calculated.each do |filename| begin source_hash[filename] = Solargraph::Source.load(filename) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 625e41ce4..ffe7da4c3 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -8,45 +8,55 @@ module Solargraph module Yardoc module_function - # Build and cache a gem's yardoc and return the path. If the cache already - # exists, do nothing and return the path. + # Build and save a gem's yardoc into a given path. # - # @param yard_plugins [Array] The names of YARD plugins to use. + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param yard_plugins [Array] # @param gemspec [Gem::Specification] - # @return [String] The path to the cached yardoc. - def cache(yard_plugins, gemspec) - path = PinCache.yardoc_path gemspec - return path if cached?(gemspec) + # + # @return [void] + def build_docs gem_yardoc_path, yard_plugins, gemspec + return if docs_built?(gem_yardoc_path) - Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" - cmd = "yardoc --db #{path} --no-output --plugin solargraph" + Solargraph.logger.info "Saving yardoc for #{gemspec.name} #{gemspec.version} into #{gem_yardoc_path}" + cmd = "yardoc --db #{gem_yardoc_path} --no-output --plugin solargraph" yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel - # - # @sg-ignore RBS gem doesn't reflect that Open3.* also include - # kwopts from Process.spawn() - stdout_and_stderr_str, status = Open3.capture2e(cmd, chdir: gemspec.gem_dir) - unless status.success? - Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } - Solargraph.logger.info stdout_and_stderr_str + unless File.exist?(gemspec.gem_dir) + Solargraph.logger.info { "Bad info from gemspec - #{gemspec.gem_dir} does not exist" } + return end - path + + # @sg-ignore + stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) + return if status.success? + Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } + Solargraph.logger.info stdout_and_stderr_str + end + + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + # @param gemspec [Gem::Specification] + # @param out [IO, nil] where to log messages + # @return [Array] + def build_pins gem_yardoc_path, gemspec, out: $stderr + yardoc = load!(gem_yardoc_path) + YardMap::Mapper.new(yardoc, gemspec).map end # True if the gem yardoc is cached. # - # @param gemspec [Gem::Specification] - def cached?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'complete') + # @param gem_yardoc_path [String] + def docs_built? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'complete') File.exist?(yardoc) end # True if another process is currently building the yardoc cache. # - # @param gemspec [Gem::Specification] - def processing?(gemspec) - yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing') + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem + def processing? gem_yardoc_path + yardoc = File.join(gem_yardoc_path, 'processing') File.exist?(yardoc) end @@ -54,11 +64,28 @@ def processing?(gemspec) # # @note This method modifies the global YARD registry. # - # @param gemspec [Gem::Specification] + # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem # @return [Array] - def load!(gemspec) - YARD::Registry.load! PinCache.yardoc_path gemspec + def load! gem_yardoc_path + YARD::Registry.load! gem_yardoc_path YARD::Registry.all end + + # If the BUNDLE_GEMFILE environment variable is set, we need to + # make sure it's an absolute path, as we'll be changing + # directories. + # + # 'bundle exec' sets an absolute path here, but at least the + # overcommit gem does not, breaking on-the-fly documention with a + # spawned yardoc command from our current bundle + # + # @return [Hash{String => String}] a hash of environment variables to override + def current_bundle_env_tweaks + tweaks = {} + if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty? + tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE']) + end + tweaks + end end end diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b03e573f0..b87970d48 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -1,80 +1,129 @@ # frozen_string_literal: true +require 'bundler' +require 'benchmark' + describe Solargraph::DocMap do - before :all do - # We use ast here because it's a known dependency. - gemspec = Gem::Specification.find_by_name('ast') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + subject(:doc_map) do + described_class.new(requires, [], workspace, out: out) 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) + let(:out) { StringIO.new } + let(:pre_cache) { true } + let(:requires) { [] } + + let(:workspace) do + Solargraph::Workspace.new(Dir.pwd) end - it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to include('not_a_gem') + let(:plain_doc_map) { described_class.new([], [], workspace, out: nil) } + + before do + doc_map.cache_doc_map_gems!(nil) if pre_cache end - it 'tracks uncached_gemspecs' do - gemspec = Gem::Specification.new do |spec| - spec.name = 'not_a_gem' - spec.version = '1.0.0' + context 'with a require in solargraph test bundle' do + let(:requires) do + ['ast'] + end + + it 'generates pins from gems' do + node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } + expect(node_pin).to be_a(Solargraph::Pin::Namespace) end - allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], [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 - workspace = Solargraph::Workspace.new(Dir.pwd) - plain_doc_map = Solargraph::DocMap.new([], [], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], [], workspace) + context 'when deserialization takes a while' do + let(:pre_cache) { false } + let(:requires) { ['backport'] } - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + before do + # proxy this method to simulate a long-running deserialization + allow(Benchmark).to receive(:measure) do |&block| + block.call + 5.0 + end + end + + it 'logs timing' do + # force lazy evaluation + _pins = doc_map.pins + expect(out.string).to include('Deserialized ').and include(' gem pins ').and include(' ms') + end end - 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).with(/path set/) - Solargraph::DocMap.new(['set'], []) + context 'with an uncached but valid gemspec' do + let(:requires) { ['uncached_gem'] } + let(:pre_cache) { false } + let(:workspace) { instance_double(Solargraph::Workspace) } + + it 'tracks uncached_gemspecs' do + pincache = instance_double(Solargraph::PinCache) + uncached_gemspec = Gem::Specification.new('uncached_gem', '1.0.0') + allow(workspace).to receive_messages(fresh_pincache: pincache) + allow(Gem::Specification).to receive(:find_by_path).with('uncached_gem').and_return(uncached_gemspec) + allow(workspace).to receive(:global_environ).and_return(Solargraph::Environ.new) + allow(pincache).to receive(:deserialize_combined_pin_cache).with(uncached_gemspec).and_return(nil) + expect(doc_map.uncached_gemspecs).to eq([uncached_gemspec]) + end end - it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + context 'with require as bundle/require' do + it 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace, out: nil) + doc_map_with_bundler_require.cache_doc_map_gems!(nil) + expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + end end - it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + context 'with a require not needed by Ruby core' do + let(:requires) { ['set'] } + + it 'does not warn' 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. + allow(Solargraph.logger).to receive(:warn) + doc_map + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end end - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + context 'with a nil require' do + let(:requires) { [nil] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end + context 'with an empty require' do + let(:requires) { [''] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error end + end - Solargraph::Convention.register dummy_convention + context 'with convention' do + let(:pre_cache) { false } - doc_map = Solargraph::DocMap.new(['original_gem'], []) + it 'includes convention requires from environ' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def global(doc_map) + Solargraph::Environ.new( + requires: ['convention_gem1', 'convention_gem2'] + ) + end + end - expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + Solargraph::Convention.register dummy_convention - # Clean up the registered convention - Solargraph::Convention.deregister dummy_convention + doc_map = Solargraph::DocMap.new(['original_gem'], [], workspace) + + expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + ensure + # Clean up the registered convention + Solargraph::Convention.deregister dummy_convention + end end end diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index d630784cf..4d2bb4ff5 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -1,14 +1,49 @@ # frozen_string_literal: true describe Solargraph::GemPins do - 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') + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:doc_map) { Solargraph::DocMap.new(requires, [], workspace, out: nil) } + let(:pin) { doc_map.pins.find { |pin| pin.path == path } } + + before do + doc_map.cache_doc_map_gems!(STDERR) # rubocop:disable Style/GlobalStdStream + end + + context 'with a combined method pin' do + let(:path) { 'RBS::EnvironmentLoader#core_root' } + let(:requires) { ['rbs'] } + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:combined) + end + + it 'finds types from RBS' do + expect(pin.return_type.to_s).to eq('Pathname, nil') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('environment_loader.rb') + end + end + + context 'with a YARD-only pin' do + let(:requires) { ['rake'] } + let(:path) { 'Rake::Task#prerequisites' } + + it 'found a pin' do + expect(pin.source).not_to be_nil + end + + it 'can merge YARD and RBS' do + expect(pin.source).to eq(:yardoc) + end + + it 'does not find types from YARD in this case' do + expect(pin.return_type.to_s).to eq('undefined') + end + + it 'finds locations from YARD' do + expect(pin.location.filename).to end_with('task.rb') + end end end diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index d59a843f1..69ee0b866 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -3,7 +3,8 @@ host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) diagnoser.schedule 'file.rb' - expect(host).to receive(:diagnose).with('file.rb') + allow(host).to receive(:diagnose) diagnoser.tick + expect(host).to have_received(:diagnose).with('file.rb') end end diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index b9ce2a41f..f7c17cb6d 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,7 +2,7 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - expect(host).to receive(:receive).with(message).and_return(nil) + allow(host).to receive(:receive).with(message).and_return(nil) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) diff --git a/spec/library_spec.rb b/spec/library_spec.rb index 34de9e1f0..f7daafdf4 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -26,6 +26,32 @@ expect(completion.pins.map(&:name)).to include('x') end + context 'with a require from a not-yet-cached external gem' do + before do + Solargraph::Shell.new.uncache('backport') + end + + it "returns a Completion", time_limit_seconds: 50 do + library = Solargraph::Library.new(Solargraph::Workspace.new(Dir.pwd, + Solargraph::Workspace::Config.new)) + library.attach Solargraph::Source.load_string(%( + require 'backport' + + # @param adapter [Backport::Adapter] + def foo(adapter) + adapter.remo + end + ), 'file.rb', 0) + completion = nil + # give Solargraph time to cache the gem + while (completion = library.completions_at('file.rb', 5, 19)).pins.empty? + sleep 0.25 + end + expect(completion).to be_a(Solargraph::SourceMap::Completion) + expect(completion.pins.map(&:name)).to include('remote') + end + end + context 'with a require from an already-cached external gem' do before do Solargraph::Shell.new.gems('backport') @@ -161,10 +187,47 @@ def bar expect(pins.map(&:path)).to include('Foo#bar') end - it "collects references to an instance method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + describe '#references_from' do + it "collects references to a new method on a constant from assignment of Class.new" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + Foo = Class.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([["file1.rb", 1]]) + end + + it "collects references to a new method to a constant from assignment" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( + Foo.new + ), 'file1.rb', 0) + library.merge src1 + src2 = Solargraph::Source.load_string(%( + class Foo + end + blah = Foo.new + ), 'file2.rb', 0) + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 3, 21) + expect(locs.map { |l| [l.filename, l.range.start.line] }) + .to eq([["file1.rb", 1], ["file2.rb", 3]]) + end + + it "collects references to an instance method symbol" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end @@ -172,8 +235,8 @@ def bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new foo.bar class Other @@ -181,17 +244,17 @@ def bar; end end Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 2, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 6}).to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 2, 11) + expect(locs.length).to eq(3) + expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 6}).to be_empty + end - it "collects references to a class method symbol" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it "collects references to a class method symbol" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def self.bar end @@ -203,8 +266,8 @@ def bar Foo.bar Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( Foo.bar Foo.new.bar class Other @@ -214,48 +277,48 @@ def bar; end Other.bar Other.new.bar ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file2.rb', 1, 11) - expect(locs.length).to eq(3) - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 2}).not_to be_empty - expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 9}).not_to be_empty - expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 1}).not_to be_empty - end + library.merge src2 + library.catalog + locs = library.references_from('file2.rb', 1, 11) + expect(locs.length).to eq(3) + expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 2}).not_to be_empty + expect(locs.select{|l| l.filename == 'file1.rb' && l.range.start.line == 9}).not_to be_empty + expect(locs.select{|l| l.filename == 'file2.rb' && l.range.start.line == 1}).not_to be_empty + end - it "collects stripped references to constant symbols" do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - src1 = Solargraph::Source.load_string(%( + it "collects stripped references to constant symbols" do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + src1 = Solargraph::Source.load_string(%( class Foo def bar end end Foo.new.bar ), 'file1.rb', 0) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( class Other foo = Foo.new foo.bar end ), 'file2.rb', 0) - library.merge src2 - library.catalog - locs = library.references_from('file1.rb', 1, 12, strip: true) - expect(locs.length).to eq(3) - locs.each do |l| - code = library.read_text(l.filename) - o1 = Solargraph::Position.to_offset(code, l.range.start) - o2 = Solargraph::Position.to_offset(code, l.range.ending) - expect(code[o1..o2-1]).to eq('Foo') + library.merge src2 + library.catalog + locs = library.references_from('file1.rb', 1, 12, strip: true) + expect(locs.length).to eq(3) + locs.each do |l| + code = library.read_text(l.filename) + o1 = Solargraph::Position.to_offset(code, l.range.start) + o2 = Solargraph::Position.to_offset(code, l.range.ending) + expect(code[o1..o2-1]).to eq('Foo') + end end - end - it 'rejects new references from different classes' do - workspace = Solargraph::Workspace.new('*') - library = Solargraph::Library.new(workspace) - source = Solargraph::Source.load_string(%( + it 'rejects new references from different classes' do + workspace = Solargraph::Workspace.new('*') + library = Solargraph::Library.new(workspace) + source = Solargraph::Source.load_string(%( class Foo def bar end @@ -263,106 +326,131 @@ def bar Foo.new Array.new ), 'test.rb') - library.merge source - library.catalog - foo_new_locs = library.references_from('test.rb', 5, 10) - expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) - obj_new_locs = library.references_from('test.rb', 6, 12) - expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) - end + library.merge source + library.catalog + foo_new_locs = library.references_from('test.rb', 5, 10) + expect(foo_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 13))]) + obj_new_locs = library.references_from('test.rb', 6, 12) + expect(obj_new_locs).to eq([Solargraph::Location.new('test.rb', Solargraph::Range.from_to(6, 12, 6, 15))]) + end - it "searches the core for queries" do - library = Solargraph::Library.new - result = library.search('String') - expect(result).not_to be_empty - end + it "searches the core for queries" do + library = Solargraph::Library.new + result = library.search('String') + expect(result).not_to be_empty + end - it "returns YARD documentation from the core" do - library = Solargraph::Library.new - api_map, result = library.document('String') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + it "returns YARD documentation from the core" do + library = Solargraph::Library.new + api_map, result = library.document('String') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "returns YARD documentation from sources" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it "returns YARD documentation from sources" do + library = Solargraph::Library.new + src = Solargraph::Source.load_string(%( class Foo # My bar method def bar; end end ), 'test.rb', 0) - library.attach src - api_map, result = library.document('Foo#bar') - expect(result).not_to be_empty - expect(result.first).to be_a(Solargraph::Pin::Base) - end + library.attach src + api_map, result = library.document('Foo#bar') + expect(result).not_to be_empty + expect(result.first).to be_a(Solargraph::Pin::Base) + end - it "synchronizes sources from updaters" do - library = Solargraph::Library.new - src = Solargraph::Source.load_string(%( + it "synchronizes sources from updaters" do + library = Solargraph::Library.new + src = Solargraph::Source.load_string(%( class Foo end ), 'test.rb', 1) - library.attach src - repl = %( + library.attach src + repl = %( class Foo def bar; end end ) - updater = Solargraph::Source::Updater.new( - 'test.rb', - 2, - [Solargraph::Source::Change.new(nil, repl)] - ) - library.attach src.synchronize(updater) - expect(library.current.code).to eq(repl) - end + updater = Solargraph::Source::Updater.new( + 'test.rb', + 2, + [Solargraph::Source::Change.new(nil, repl)] + ) + library.attach src.synchronize(updater) + expect(library.current.code).to eq(repl) + end - it "finds unique references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - src1 = Solargraph::Source.load_string(%( + it "finds unique references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + src1 = Solargraph::Source.load_string(%( class Foo end ), 'src1.rb', 1) - library.merge src1 - src2 = Solargraph::Source.load_string(%( + library.merge src1 + src2 = Solargraph::Source.load_string(%( foo = Foo.new ), 'src2.rb', 1) - library.merge src2 - library.catalog - refs = library.references_from('src2.rb', 1, 12) - expect(refs.length).to eq(2) - end + library.merge src2 + library.catalog + refs = library.references_from('src2.rb', 1, 12) + expect(refs.length).to eq(2) + end - it "includes method parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it "includes method parameters in references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( class Foo def bar(baz) baz.upcase end end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 2, 16) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 3, 10) - expect(from_ref.length).to eq(2) - end + library.attach source + from_def = library.references_from('test.rb', 2, 16) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 3, 10) + expect(from_ref.length).to eq(2) + end - it "includes block parameters in references" do - library = Solargraph::Library.new(Solargraph::Workspace.new('*')) - source = Solargraph::Source.load_string(%( + it "lies about names when client can't handle the truth" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: true) + expect(from_def.first.range.start.column).to eq(14) + end + + it "tells the truth about names when client can handle the truth" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( + class Foo + def 🤦🏻foo♀️; 123; end + end + ), 'test.rb', 1) + library.attach source + from_def = library.references_from('test.rb', 2, 16, strip: false) + expect(from_def.first.range.start.column).to eq(12) + end + + it "includes block parameters in references" do + library = Solargraph::Library.new(Solargraph::Workspace.new('*')) + source = Solargraph::Source.load_string(%( 100.times do |foo| puts foo end ), 'test.rb', 1) - library.attach source - from_def = library.references_from('test.rb', 1, 20) - expect(from_def.length).to eq(2) - from_ref = library.references_from('test.rb', 2, 13) - expect(from_ref.length).to eq(2) + library.attach source + from_def = library.references_from('test.rb', 1, 20) + expect(from_def.length).to eq(2) + from_ref = library.references_from('test.rb', 2, 13) + expect(from_ref.length).to eq(2) + end end it 'defines YARD tags' do diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb new file mode 100644 index 000000000..0a11686f5 --- /dev/null +++ b/spec/pin_cache_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'bundler' +require 'benchmark' + +describe Solargraph::PinCache do + subject(:pin_cache) do + described_class.new(rbs_collection_path: '.gem_rbs_collection', + rbs_collection_config_path: 'rbs_collection.yaml', + directory: Dir.pwd, + yard_plugins: ['activesupport-concern']) + end + + describe '#cached?' do + it 'returns true for a gem that is cached' do + allow(File).to receive(:file?).with(%r{.*stdlib/backport.ser$}).and_return(false) + allow(File).to receive(:file?).with(%r{.*combined/.*/backport-.*.ser$}).and_return(true) + + gemspec = Gem::Specification.find_by_name('backport') + expect(pin_cache.cached?(gemspec)).to be true + end + + it 'returns false for a gem that is not cached' do + gemspec = Gem::Specification.new.tap do |spec| + spec.name = 'nonexistent' + spec.version = '0.0.1' + end + expect(pin_cache.cached?(gemspec)).to be false + end + end + + describe '.core?' do + it 'returns true when core pins exist' do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(true) + + expect(described_class.core?).to be true + end + + it "returns true when core pins don't" do + allow(File).to receive(:file?).with(%r{.*/core.ser$}).and_return(false) + + expect(described_class.core?).to be false + end + end + + describe '#possible_stdlibs' do + it 'is tolerant of less usual Ruby installations' do + stub_const('Gem::RUBYGEMS_DIR', nil) + + expect(pin_cache.possible_stdlibs).to eq([]) + end + end + + describe '#cache_all_stdlibs' do + it 'creates stdlibmaps' do + allow(Solargraph::RbsMap::StdlibMap).to receive(:new).and_return(instance_double(Solargraph::RbsMap::StdlibMap)) + + pin_cache.cache_all_stdlibs + + expect(Solargraph::RbsMap::StdlibMap).to have_received(:new).at_least(:once) + end + end + + describe '#cache_gem' do + context 'with an already in-memory gem' do + let(:backport_gemspec) { Gem::Specification.find_by_name('backport') } + + before do + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + end + + it 'does not load the gem again' do + allow(Marshal).to receive(:load).and_call_original + + pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + + expect(Marshal).not_to have_received(:load).with(anything) + end + end + + context 'with the parser gem' do + before do + Solargraph::Shell.new.uncache('parser') + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with an installed gem' do + before do + Solargraph::Shell.new.gems('kramdown') + end + + it 'uncaches when asked' do + gemspec = Gem::Specification.find_by_name('kramdown') + expect do + pin_cache.uncache_gem(gemspec, out: nil) + end.not_to raise_error + end + end + + context 'with the rebuild flag' do + before do + allow(Solargraph::Yardoc).to receive(:build_docs) + end + + it 'chooses not to use YARD' do + parser_gemspec = Gem::Specification.find_by_name('parser') + pin_cache.cache_gem(gemspec: parser_gemspec, rebuild: true, out: nil) + # if this fails, you may not have run `bundle exec rbs collection update` + expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) + end + end + + context 'with a stdlib gem' do + let(:gem_name) { 'logger' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/logger-.*-stdlib.ser$}, any_args).once + end + end + + context 'with gem packaged with its own RBS gem' do + let(:gem_name) { 'base64' } + + before do + Solargraph::Shell.new.uncache(gem_name) + end + + it 'caches' do + yaml_gemspec = Gem::Specification.find_by_name(gem_name) + allow(File).to receive(:write).and_call_original + + pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + + # match arguments with regexp using rspec-matchers syntax + expect(File).to have_received(:write).with(%r{combined/.*/base64-.*-export.ser$}, any_args, mode: 'wb').once + end + end + end + + describe '#uncache_gem' do + subject(:call) { pin_cache.uncache_gem(gemspec, out: out) } + + let(:out) { StringIO.new } + + before do + allow(FileUtils).to receive(:rm_rf) + end + + context 'with an already cached gem' do + let(:gemspec) { Gem::Specification.find_by_name('backport') } + + it 'deletes files' do + call + + expect(FileUtils).to have_received(:rm_rf).at_least(:once) + end + end + + context 'with a non-existent gem' do + let(:gemspec) { instance_double(Gem::Specification, name: 'nonexistent', version: '0.0.1') } + + it 'does not raise an error' do + expect { call }.not_to raise_error + end + + it 'logs a message' do + call + + expect(out.string).to include('does not exist') + end + + it 'does not delete files' do + call + + expect(FileUtils).not_to have_received(:rm_rf) + end + end + end +end diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index b06c975d1..f3ca90a36 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -3,7 +3,18 @@ spec = Gem::Specification.find_by_name('rbs') rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) pin = rbs_map.path_pin('RBS::EnvironmentLoader#add_collection') - expect(pin).to be + expect(pin).not_to be_nil + end + + it 'fails if it does not find data from gemspec' do + spec = Gem::Specification.find_by_name('backport') + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + expect(rbs_map).not_to be_resolved + end + + it 'fails if it does not find data from name' do + rbs_map = Solargraph::RbsMap.new('lskdflaksdfjl') + expect(rbs_map.pins).to be_empty end it 'converts constants and aliases to correct types' do diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 91f84b4c7..6e2a83074 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + require 'tmpdir' require 'open3' describe Solargraph::Shell do + let(:shell) { described_class.new } let(:temp_dir) { Dir.mktmpdir } before do @@ -25,20 +28,122 @@ def bundle_exec(*cmd) FileUtils.rm_rf(temp_dir) end - describe "--version" do - it "returns a version when run" do - output = bundle_exec("solargraph", "--version") + describe '--version' do + let(:output) { bundle_exec('solargraph', '--version') } + it 'returns output' do expect(output).not_to be_empty + end + + it 'returns a version when run' do expect(output).to eq("#{Solargraph::VERSION}\n") end end - describe "uncache" do - it "uncaches without erroring out" do - output = bundle_exec("solargraph", "uncache", "solargraph") + describe 'uncache' do + it 'uncaches without erroring out' do + output = capture_stdout do + shell.uncache('backport') + end expect(output).to include('Clearing pin cache in') end + + it 'uncaches stdlib without erroring out' do + expect { shell.uncache('stdlib') }.not_to raise_error + end + + it 'uncaches core without erroring out' do + expect { shell.uncache('core') }.not_to raise_error + end + end + + describe 'scan' do + context 'with mocked dependencies' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + end + + it 'scans without erroring out' do + allow(api_map).to receive(:pins).and_return([]) + output = capture_stdout do + shell.options = { directory: 'spec/fixtures/workspace' } + shell.scan + end + + expect(output).to include('Scanned ').and include(' seconds.') + end + end + end + + describe 'typecheck' do + context 'with mocked dependencies' do + let(:type_checker) { instance_double(Solargraph::TypeChecker) } + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(Solargraph::TypeChecker).to receive(:new).and_return(type_checker) + allow(type_checker).to receive(:problems).and_return([]) + end + + it 'typechecks without erroring out' do + output = capture_stdout do + shell.options = { level: 'normal', directory: '.' } + shell.typecheck('Gemfile') + end + + expect(output).to include('Typecheck finished in') + end + end + end + + describe 'gems' do + context 'without mocked ApiMap' do + it 'complains when gem does not exist' do + output = capture_both do + shell.gems('nonexistentgem') + end + + expect(output).to include("Gem 'nonexistentgem' not found") + end + + it 'caches core without erroring out' do + pending('https://github.com/castwide/solargraph/pull/1061') + + capture_both do + shell.uncache('core') + end + + expect { shell.cache('core') }.not_to raise_error + end + + it 'gives sensible error for gem that does not exist' do + output = capture_both do + shell.gems('solargraph123') + end + + expect(output).to include("Gem 'solargraph123' not found") + end + end + end + + describe 'cache' do + it 'caches a stdlib gem without erroring out' do + expect { shell.cache('stringio') }.not_to raise_error + end + + context 'when gem does not exist' do + subject(:call) { shell.cache('nonexistentgem8675309') } + + it 'gives a good error message' do + pending('https://github.com/castwide/solargraph/pull/1061') + + # capture stderr output + expect { call }.to output(/not found/).to_stderr + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..0a0c1dde4 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,7 +26,9 @@ c.example_status_persistence_file_path = 'rspec-examples.txt' end require 'solargraph' -# Suppress logger output in specs (if possible) +# execute any logging blocks to make sure they don't blow up +Solargraph::Logging.logger.sev_threshold = Logger::DEBUG +# ...but still suppress logger output in specs (if possible) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') Solargraph::Logging.logger.reopen(File::NULL) end @@ -43,3 +45,29 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end diff --git a/spec/type_checker/levels/normal_spec.rb b/spec/type_checker/levels/normal_spec.rb index 3b38f55d8..0b3024f62 100644 --- a/spec/type_checker/levels/normal_spec.rb +++ b/spec/type_checker/levels/normal_spec.rb @@ -1,5 +1,5 @@ describe Solargraph::TypeChecker do - context 'normal level' do + context 'when checking at normal level' do def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :normal) end @@ -221,9 +221,9 @@ def bar; 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. + workspace = Solargraph::Workspace.new(Dir.pwd) gemspec = Gem::Specification.find_by_name('kramdown-parser-gfm') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + workspace.cache_gem(gemspec) checker = type_checker(%( require 'kramdown-parser-gfm' diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 6b00e5c33..3afa3a4ca 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -1,4 +1,14 @@ describe Solargraph::YardMap::Mapper do + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load('.') + end + + def pins_with require + doc_map = Solargraph::DocMap.new([require], [], @api_map.workspace, out: nil) # rubocop:disable RSpec/InstanceVariable + doc_map.cache_doc_map_gems!(nil) + doc_map.pins + end + it 'converts nil docstrings to empty strings' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) Dir.chdir dir do @@ -14,50 +24,33 @@ it 'marks explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.cache([], rspec) - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + pin = pins_with('rspec/expectations').find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + expect(pin).not_to be_nil expect(pin.explicit?).to be(true) end it 'marks correct return type from Logger.new' do # Using logger because it's a known dependency - logger = Gem::Specification.find_by_name('logger') - Solargraph::Yardoc.cache([], logger) - registry = Solargraph::Yardoc.load!(logger) - pins = Solargraph::YardMap::Mapper.new(registry).map - pins = pins.select { |pin| pin.path == 'Logger.new' } + pins = pins_with('logger').select { |pin| pin.path == 'Logger.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks correct return type from RuboCop::Options.new' do # Using rubocop because it's a known dependency - rubocop = Gem::Specification.find_by_name('rubocop') - Solargraph::Yardoc.cache([], rubocop) - Solargraph::Yardoc.load!(rubocop) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pins = pins.select { |pin| pin.path == 'RuboCop::Options.new' } + pins = pins_with('rubocop').select { |pin| pin.path == 'RuboCop::Options.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) expect(pins.flat_map(&:signatures).map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks non-explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#expect' } + pin = pins_with('rspec/expectations').find { |pin| pin.path == 'RSpec::Matchers#expect' } expect(pin.explicit?).to be(false) end it 'adds superclass references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - pin = pins.find do |pin| + pin = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.name == 'YARD::CodeObjects::NamespaceObject' end expect(pin.closure.path).to eq('YARD::CodeObjects::ClassObject') @@ -65,10 +58,7 @@ it 'adds include references' do # Asssuming the ast gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('ast') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - inc= pins.find do |pin| + inc = pins_with('ast').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'AST::Processor::Mixin' && pin.closure.path == 'AST::Processor' end expect(inc).to be_a(Solargraph::Pin::Reference::Include) @@ -76,10 +66,7 @@ it 'adds extend references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - ext = pins.find do |pin| + ext = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Extend) && pin.name == 'Enumerable' && pin.closure.path == 'YARD::Registry' end expect(ext).to be_a(Solargraph::Pin::Reference::Extend) diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb new file mode 100644 index 000000000..2e821498e --- /dev/null +++ b/spec/yardoc_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'open3' + +describe Solargraph::Yardoc do + around do |testobj| + @tmpdir = Dir.mktmpdir + + testobj.run + ensure + FileUtils.remove_entry(@tmpdir) # rubocop:disable RSpec/InstanceVariable + end + + let(:gem_yardoc_path) do + File.join(@tmpdir, 'solargraph', 'yardoc', 'test_gem') # rubocop:disable RSpec/InstanceVariable + end + + before do + FileUtils.mkdir_p(gem_yardoc_path) + end + + describe '#processing?' do + it 'returns true if the yardoc is being processed' do + FileUtils.touch(File.join(gem_yardoc_path, 'processing')) + expect(described_class.processing?(gem_yardoc_path)).to be(true) + end + + it 'returns false if the yardoc is not being processed' do + expect(described_class.processing?(gem_yardoc_path)).to be(false) + end + end + + describe '#load!' do + it 'does not blow up when called on empty directory' do + expect { described_class.load!(gem_yardoc_path) }.not_to raise_error + end + end + + describe '#build_docs' do + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:gemspec) { workspace.find_gem('rubocop') } + let(:output) { '' } + + before do + allow(Solargraph.logger).to receive(:warn) + allow(Solargraph.logger).to receive(:info) + FileUtils.rm_rf(gem_yardoc_path) + end + + it 'builds docs for a gem' do + described_class.build_docs(gem_yardoc_path, [], gemspec) + expect(File.exist?(File.join(gem_yardoc_path, 'complete'))).to be true + end + + it 'bails quietly if directory given does not exist' do + allow(File).to receive(:exist?).and_return(false) + + expect do + described_class.build_docs(gem_yardoc_path, [], gemspec) + end.not_to raise_error + end + + it 'is idempotent' do + described_class.build_docs(gem_yardoc_path, [], gemspec) + described_class.build_docs(gem_yardoc_path, [], gemspec) # second time + expect(File.exist?(File.join(gem_yardoc_path, 'complete'))).to be true + end + + context 'with an error from yard' do + before do + allow(Open3).to receive(:capture2e).and_return([output, result]) + end + + let(:result) { instance_double(Process::Status) } + + it 'does not raise on error from yard' do + allow(result).to receive(:success?).and_return(false) + + expect do + described_class.build_docs(gem_yardoc_path, [], gemspec) + end.not_to raise_error + end + end + + context 'when given a relative BUNDLE_GEMFILE path' do + around do |example| + # turn absolute BUNDLE_GEMFILE path into relative + existing_gemfile = ENV.fetch('BUNDLE_GEMFILE', nil) + current_dir = Dir.pwd + # remove prefix current_dir from path + ENV['BUNDLE_GEMFILE'] = existing_gemfile.sub("#{current_dir}/", '') + raise 'could not figure out relative path' if Pathname.new(ENV.fetch('BUNDLE_GEMFILE', nil)).absolute? + example.run + ENV['BUNDLE_GEMFILE'] = existing_gemfile + end + + it 'sends Open3 an absolute path' do + called_with = nil + allow(Open3).to receive(:capture2e) do |*args| + called_with = args + ['output', instance_double(Process::Status, success?: true)] + end + + described_class.build_docs(gem_yardoc_path, [], gemspec) + + expect(called_with[0]['BUNDLE_GEMFILE']).to start_with('/') + end + end + end +end From c7b66cf1ae782dda517f6d262551772b4b19af76 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 3 Sep 2025 20:47:37 -0400 Subject: [PATCH 097/524] Exclude more scenarios that RBS does not support --- .github/workflows/rspec.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 7b2255b0e..4038be21a 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,7 +29,15 @@ jobs: rbs-version: '3.9.4' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' - steps: + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS dev release + - ruby-version: 'head' + rbs-version: '4.0.0.dev.4' + - ruby-version: 'head' + rbs-version: '3.9.4' + - ruby-version: 'head' + rbs-version: '3.6.1' + steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 From eb676b70da386a704f0b8cc7b09a5a26e67787d1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 3 Sep 2025 20:49:09 -0400 Subject: [PATCH 098/524] Fix indentation --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 4038be21a..4846551ea 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -37,7 +37,7 @@ jobs: rbs-version: '3.9.4' - ruby-version: 'head' rbs-version: '3.6.1' - steps: + steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 From 8ea210411fce0b2fcb8cf4d0b392a98454a2baad Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 3 Sep 2025 23:02:00 -0400 Subject: [PATCH 099/524] Adjust comment location --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 4846551ea..0c83e8434 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -23,8 +23,8 @@ jobs: matrix: ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', 'head'] rbs-version: ['3.6.1', '3.9.4', '4.0.0.dev.4'] - # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: + # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 - ruby-version: '3.0' rbs-version: '3.9.4' - ruby-version: '3.0' From 2b3152e32da43a8fafe3b6435ba6e1035d5a06a8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 3 Sep 2025 23:03:04 -0400 Subject: [PATCH 100/524] Ensure using latest RBS version in undercover --- .github/workflows/rspec.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 0c83e8434..6fe05a9b9 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -74,7 +74,9 @@ jobs: ruby-version: '3.4' bundler-cache: false - name: Install gems - run: bundle install + run: | + bundle install + bundle update rbs # use latest available for this Ruby version - name: Install types run: bundle exec rbs collection update - name: Run tests From 1b644b48452e7875233765bc0f2c40361cf84308 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 09:09:19 -0400 Subject: [PATCH 101/524] Add regression specs --- spec/api_map/index_spec.rb | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 spec/api_map/index_spec.rb diff --git a/spec/api_map/index_spec.rb b/spec/api_map/index_spec.rb new file mode 100644 index 000000000..8afb74759 --- /dev/null +++ b/spec/api_map/index_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +describe Solargraph::ApiMap::Index do + subject(:output_pins) { described_class.new(input_pins).pins } + + describe '#map_overrides' do + let(:foo_class) do + Solargraph::Pin::Namespace.new(name: 'Foo') + end + + let(:foo_initialize) do + init = Solargraph::Pin::Method.new(name: 'initialize', + scope: :instance, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_new) do + init = Solargraph::Pin::Method.new(name: 'new', + scope: :class, + parameters: [], + closure: foo_class) + # no return type specified + param = Solargraph::Pin::Parameter.new(name: 'bar', + closure: init) + init.parameters << param + init + end + + let(:foo_override) do + Solargraph::Pin::Reference::Override.from_comment('Foo#initialize', + '@param [String] bar') + end + + let(:input_pins) do + [ + foo_initialize, + foo_new, + foo_override + ] + end + + it 'has a docstring to process on override' do + expect(foo_override.docstring.tags).to be_empty + end + + it 'overrides .new method' do + method_pin = output_pins.find { |pin| pin.path == 'Foo.new' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + + it 'overrides #initialize method in signature' do + method_pin = output_pins.find { |pin| pin.path == 'Foo#initialize' } + first_parameter = method_pin.parameters.first + expect(first_parameter.return_type.tag).to eq('String') + end + end +end From aafaa5d867458d3bb2895387cfc80d067ccd50ad Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 10:49:29 -0400 Subject: [PATCH 102/524] Ensure overrides are handled properly regardless of state of pin --- lib/solargraph/api_map/index.rb | 12 +++++++----- lib/solargraph/pin/callable.rb | 5 +++++ lib/solargraph/pin/parameter.rb | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index a5870ff50..8234e4718 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -169,10 +169,13 @@ def map_overrides 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 + pin.reset_generated! + + next unless new_pin + + new_pin.docstring.add_tag(tag) + redefine_return_type new_pin, tag + new_pin.reset_generated! end end end @@ -189,7 +192,6 @@ def redefine_return_type pin, tag 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/pin/callable.rb b/lib/solargraph/pin/callable.rb index 504dd4862..7d9fd69be 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -205,6 +205,11 @@ def arity_matches? arguments, with_block true end + def reset_generated! + super + @parameters.each(&:reset_generated!) + end + # @return [Integer] def mandatory_positional_param_count parameters.count(&:arg?) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 512ee0ead..9f58ec067 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -123,6 +123,11 @@ def full_name end end + def reset_generated! + super + @return_type = nil if @return_type&.undefined? + end + # @return [String] def full full_name + case decl From e2ba56923e5e4e26d997a89447cf8fdd8c6433e8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 11:03:01 -0400 Subject: [PATCH 103/524] Lint fixes --- spec/yard_map/mapper_spec.rb | 2 +- spec/yardoc_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 3afa3a4ca..770d9e9ef 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -4,7 +4,7 @@ end def pins_with require - doc_map = Solargraph::DocMap.new([require], [], @api_map.workspace, out: nil) # rubocop:disable RSpec/InstanceVariable + doc_map = Solargraph::DocMap.new([require], [], @api_map.workspace, out: nil) doc_map.cache_doc_map_gems!(nil) doc_map.pins end diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb index 2e821498e..6e7171afe 100644 --- a/spec/yardoc_spec.rb +++ b/spec/yardoc_spec.rb @@ -9,11 +9,11 @@ testobj.run ensure - FileUtils.remove_entry(@tmpdir) # rubocop:disable RSpec/InstanceVariable + FileUtils.remove_entry(@tmpdir) end let(:gem_yardoc_path) do - File.join(@tmpdir, 'solargraph', 'yardoc', 'test_gem') # rubocop:disable RSpec/InstanceVariable + File.join(@tmpdir, 'solargraph', 'yardoc', 'test_gem') end before do From 5b6a4af7d6474be1cbad660932d95fff6071a098 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 12:46:07 -0400 Subject: [PATCH 104/524] Extract gemspec, bundle and dependency management into its own class --- .rubocop_todo.yml | 31 +---- lib/solargraph/api_map.rb | 30 +++-- lib/solargraph/doc_map.rb | 142 ++------------------- lib/solargraph/workspace.rb | 39 ++++-- lib/solargraph/workspace/gemspecs.rb | 184 +++++++++++++++++++++++++++ spec/doc_map_spec.rb | 23 ++-- spec/workspace_spec.rb | 4 +- 7 files changed, 259 insertions(+), 194 deletions(-) create mode 100644 lib/solargraph/workspace/gemspecs.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b8d123824..0b3e1fd0e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.80.0. +# using RuboCop version 1.80.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -64,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -98,7 +97,6 @@ Layout/ElseAlignment: # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' @@ -107,7 +105,6 @@ Layout/EmptyLines: Exclude: - 'lib/solargraph/bench.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/extended/check_gem_version.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' @@ -225,7 +222,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -920,9 +916,9 @@ RSpec/DescribeClass: - '**/spec/routing/**/*' - '**/spec/system/**/*' - '**/spec/views/**/*' + - 'spec/api_map_method_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/source_map/node_processor_spec.rb' - - 'spec/api_map_method_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. @@ -1045,7 +1041,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/convention/struct_definition_spec.rb' - 'spec/pin/base_spec.rb' - 'spec/pin/method_spec.rb' @@ -1086,7 +1081,6 @@ RSpec/ImplicitExpect: RSpec/InstanceVariable: Exclude: - 'spec/api_map/config_spec.rb' - - 'spec/api_map_spec.rb' - 'spec/diagnostics/require_not_found_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/host_spec.rb' @@ -2225,12 +2219,6 @@ Style/RedundantFreeze: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/source_map/mapper.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowComments. -Style/RedundantInitialize: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -2246,6 +2234,7 @@ Style/RedundantParentheses: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' + - 'lib/solargraph/pin/search.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/type_checker.rb' @@ -2258,24 +2247,13 @@ Style/RedundantRegexpArgument: - 'spec/diagnostics/rubocop_spec.rb' - 'spec/language_server/host_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'lib/solargraph/source/cursor.rb' - - 'lib/solargraph/source/source_chainer.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/diagnostics/rubocop.rb' - 'lib/solargraph/language_server/uri_helpers.rb' - - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/pin/parameter.rb' - 'lib/solargraph/shell.rb' - - 'lib/solargraph/source/change.rb' - - 'lib/solargraph/source/cursor.rb' - 'lib/solargraph/source_map/clip.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2337,7 +2315,7 @@ Style/SafeNavigation: # Configuration parameters: Max. Style/SafeNavigationChainLength: Exclude: - - 'lib/solargraph/doc_map.rb' + - 'lib/solargraph/workspace/gemspecs.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: @@ -2638,7 +2616,6 @@ YARD/MismatchName: Exclude: - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - - 'lib/solargraph/gem_pins.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/language_server/host/dispatch.rb' - 'lib/solargraph/language_server/request.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f58633a0c..c2e0967f7 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -22,6 +22,9 @@ class ApiMap # @return [Array] attr_reader :missing_docs + # @return [Solargraph::Workspace::Gemspecs] + attr_reader :gemspecs + # @param pins [Array] def initialize pins: [] @source_map_hash = {} @@ -99,7 +102,7 @@ def catalog bench @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) # @todo Implement gem preferences + @doc_map = DocMap.new(unresolved_requires, bench.workspace) # @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) @@ -116,7 +119,7 @@ def catalog bench # @return [DocMap] def doc_map - @doc_map ||= DocMap.new([], []) + @doc_map ||= DocMap.new([], Workspace.new('.')) end # @return [::Array] @@ -212,6 +215,7 @@ class << self # # @param directory [String] # @param out [IO] The output stream for messages + # # @return [ApiMap] def self.load_with_cache directory, out api_map = load(directory) @@ -533,7 +537,8 @@ def get_complex_type_methods complex_type, context = '', internal = false # @param name [String] Method name to look up # @param scope [Symbol] :instance or :class # @param visibility [Array] :public, :protected, and/or :private - # @param preserve_generics [Boolean] + # @param preserve_generics [Boolean] True to preserve any + # unresolved generic parameters, false to erase them # @return [Array] def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false rooted_type = ComplexType.parse(rooted_tag) @@ -559,7 +564,7 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, # @deprecated Use #get_path_pins instead. # # @param path [String] The path to find - # @return [Enumerable] + # @return [Array] def get_path_suggestions path return [] if path.nil? resolve_method_aliases store.get_path_pins(path) @@ -568,7 +573,7 @@ def get_path_suggestions path # Get an array of pins that match the specified path. # # @param path [String] - # @return [Enumerable] + # @return [Array] def get_path_pins path get_path_suggestions(path) end @@ -658,7 +663,7 @@ def bundled? filename # @param sup [String] The superclass # @param sub [String] The subclass # @return [Boolean] - def super_and_sub?(sup, sub) + def super_and_sub? sup, sub fqsup = qualify(sup) cls = qualify(sub) tested = [] @@ -677,7 +682,7 @@ def super_and_sub?(sup, sub) # @param module_ns [String] The module namespace (no type parameters) # # @return [Boolean] - def type_include?(host_ns, module_ns) + def type_include? host_ns, module_ns store.get_includes(host_ns).map { |inc_tag| ComplexType.parse(inc_tag).name }.include?(module_ns) end @@ -695,6 +700,11 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] GemPins.combine_method_pins_by_path(with_resolved_aliases) end + # @return [Workspace, nil] + def workspace + @doc_map&.workspace + end + # @param fq_reference_tag [String] A fully qualified whose method should be pulled in # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type # parameter - used to pull generics information @@ -1038,18 +1048,18 @@ def erase_generics(namespace_pin, rooted_type, pins) # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def should_erase_generics_when_done?(namespace_pin, rooted_type) + def should_erase_generics_when_done? namespace_pin, rooted_type has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type) end # @param namespace_pin [Pin::Namespace] - def has_generics?(namespace_pin) + def has_generics? namespace_pin namespace_pin && !namespace_pin.generics.empty? end # @param namespace_pin [Pin::Namespace] # @param rooted_type [ComplexType] - def can_resolve_generics?(namespace_pin, rooted_type) + def can_resolve_generics? namespace_pin, rooted_type has_generics?(namespace_pin) && !rooted_type.all_params.empty? end end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5fe5e03f9..306dcfcf4 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -5,7 +5,10 @@ require 'open3' module Solargraph - # A collection of pins generated from required gems. + # A collection of pins generated from specific 'require' statements + # in code. Multiple can be created per workspace, to represent the + # pins available in different files based on their particular + # 'require' lines. # class DocMap include Logging @@ -14,9 +17,6 @@ class DocMap attr_reader :requires alias required requires - # @return [Array] - attr_reader :preferences - # @return [Array] attr_reader :pins @@ -46,11 +46,10 @@ def uncached_gemspecs attr_reader :environ # @param requires [Array] - # @param preferences [Array] # @param workspace [Workspace, nil] - def initialize(requires, preferences, workspace = nil) + # @param out [IO, nil] output stream for logging + def initialize requires, workspace, out: $stderr @requires = requires.compact - @preferences = preferences.compact @workspace = workspace @rbs_collection_path = workspace&.rbs_collection_path @rbs_collection_config_path = workspace&.rbs_collection_config_path @@ -166,7 +165,7 @@ def yard_plugins # @return [Set] def dependencies - @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set + @dependencies ||= (gemspecs.flat_map { |spec| workspace.fetch_dependencies(spec) } - gemspecs).to_set end private @@ -203,12 +202,7 @@ def load_serialized_gem_pins # @return [Hash{String => Array}] def required_gems_map - @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] } - end - - # @return [Hash{String => Gem::Specification}] - def preference_map - @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } + @required_gems_map ||= requires.to_h { |path| [path, workspace.resolve_require(path)] } end # @param gemspec [Gem::Specification] @@ -307,128 +301,8 @@ def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key end end - # @param path [String] - # @return [::Array, nil] - def resolve_path_to_gemspecs path - return nil if path.empty? - return gemspecs_required_from_bundler if path == 'bundler/require' - - # @type [Gem::Specification, nil] - gemspec = Gem::Specification.find_by_path(path) - if gemspec.nil? - gem_name_guess = path.split('/').first - begin - # this can happen when the gem is included via a local path in - # a Gemfile; Gem doesn't try to index the paths in that case. - # - # See if we can make a good guess: - potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) - file = "lib/#{path}.rb" - gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } - rescue Gem::MissingSpecError - logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" } - [] - end - end - return nil if gemspec.nil? - [gemspec_or_preference(gemspec)] - end - - # @param gemspec [Gem::Specification] - # @return [Gem::Specification] - def gemspec_or_preference gemspec - return gemspec unless preference_map.key?(gemspec.name) - return gemspec if gemspec.version == preference_map[gemspec.name].version - - change_gemspec_version gemspec, preference_map[by_path.name].version - end - - # @param gemspec [Gem::Specification] - # @param version [Gem::Version] - # @return [Gem::Specification] - def change_gemspec_version gemspec, version - Gem::Specification.find_by_name(gemspec.name, "= #{version}") - rescue Gem::MissingSpecError - Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead" - gemspec - end - - # @param gemspec [Gem::Specification] - # @return [Array] - def fetch_dependencies gemspec - # @param spec [Gem::Dependency] - only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| - Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" - dep = Gem.loaded_specs[spec.name] - # @todo is next line necessary? - 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 in RubyGems." - end.to_a - end - - # @param gemspec [Gem::Specification] - # @return [Array] - def only_runtime_dependencies gemspec - gemspec.dependencies - gemspec.development_dependencies - end - - def inspect self.class.inspect end - - # @return [Array] - def gemspecs_required_from_bundler - # @todo Handle projects with custom Bundler/Gemfile setups - return unless workspace.gemfile? - - if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory) - # Find only the gems bundler is now using - Bundler.definition.locked_gems.specs.flat_map do |lazy_spec| - logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}" - [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)] - rescue Gem::MissingSpecError => e - logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs lazy_spec.name - logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - logger.info 'Fetching gemspecs required from Bundler (bundler/require)' - gemspecs_required_from_external_bundle - end - end - - # @return [Array] - def gemspecs_required_from_external_bundle - logger.info 'Fetching gemspecs required from external bundle' - return [] unless workspace&.directory - - Solargraph.with_clean_env do - cmd = [ - 'ruby', '-e', - "require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }" - ] - o, e, s = Open3.capture3(*cmd) - if s.success? - Solargraph.logger.debug "External bundle: #{o}" - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} - hash.flat_map do |name, version| - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError => e - logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") - # can happen in local filesystem references - specs = resolve_path_to_gemspecs name - logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - Solargraph.logger.warn "Failed to load gems from bundle at #{workspace&.directory}: #{e}" - end - end - end end end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index ffd653d96..e907a7e43 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -9,7 +9,10 @@ module Solargraph # in an associated Library or ApiMap. # class Workspace + include Logging + autoload :Config, 'solargraph/workspace/config' + autoload :Gemspecs, 'solargraph/workspace/gemspecs' # @return [String] attr_reader :directory @@ -41,6 +44,19 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @param out [IO, nil] output stream for logging + # @param gemspec [Gem::Specification] + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + gemspecs.fetch_dependencies(gemspec, out: out) + end + + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [Array] + def resolve_require require + gemspecs.resolve_require(require) + end + # Merge the source. A merge will update the existing source for the file # or add it to the sources if the workspace is configured to include it. # The source is ignored if the configuration excludes it. @@ -115,15 +131,15 @@ def would_require? path # # @return [Boolean] def gemspec? - !gemspecs.empty? + !gemspec_files.empty? end # Get an array of all gemspec files in the workspace. # # @return [Array] - def gemspecs + def gemspec_files return [] if directory.empty? || directory == '*' - @gemspecs ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs| + @gemspec_files ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs| config.allow? gs end end @@ -156,12 +172,15 @@ def command_path server['commandPath'] || 'solargraph' end - # True if the workspace has a root Gemfile. - # - # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler) - # - def gemfile? - directory && File.file?(File.join(directory, 'Gemfile')) + # @return [String, nil] + def directory_or_nil + return nil if directory.empty? || directory == '*' + directory + end + + # @return [Solargraph::Workspace::Gemspecs] + def gemspecs + @gemspecs ||= Solargraph::Workspace::Gemspecs.new(directory_or_nil) end private @@ -200,7 +219,7 @@ def load_sources def generate_require_paths return configured_require_paths unless gemspec? result = [] - gemspecs.each do |file| + gemspec_files.each do |file| base = File.dirname(file) # HACK: Evaluating gemspec files violates the goal of not running # workspace code, but this is how Gem::Specification.load does it diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb new file mode 100644 index 000000000..717c723da --- /dev/null +++ b/lib/solargraph/workspace/gemspecs.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'rubygems' +require 'bundler' + +module Solargraph + class Workspace + # Manages determining which gemspecs are available in a workspace + class Gemspecs + include Logging + + attr_reader :directory, :preferences + + # @param directory [String, nil] If nil, assume no bundle is present + # @param preferences [Array] + def initialize directory, preferences: [] + # @todo an issue with both external bundles and the potential + # preferences feature is that bundler gives you a 'clean' + # rubygems environment with only the specified versions + # installed. Possible alternatives: + # + # *) prompt the user to run solargraph outside of bundler + # and treat all bundles as external + # *) reinstall the needed gems dynamically each time + # *) manipulate the rubygems/bundler environment + @directory = directory && File.absolute_path(directory) + # @todo implement preferences as a config-exposed feature + @preferences = preferences + end + + # Take the path given to a 'require' statement in a source file + # and return the Gem::Specifications which will be brought into + # scope with it, so we can load pins for them. + # + # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require' + # @return [::Array, nil] + def resolve_require require + return nil if require.empty? + return gemspecs_required_from_bundler if require == 'bundler/require' + + # @sg-ignore Variable type could not be inferred for gemspec + # @type [Gem::Specification, nil] + gemspec = Gem::Specification.find_by_path(require) + if gemspec.nil? + gem_name_guess = require.split('/').first + begin + # this can happen when the gem is included via a local path in + # a Gemfile; Gem doesn't try to index the paths in that case. + # + # See if we can make a good guess: + potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) + file = "lib/#{require}.rb" + # @sg-ignore Unresolved call to files + gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } + rescue Gem::MissingSpecError + logger.debug do + "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" + end + [] + end + end + return nil if gemspec.nil? + [gemspec_or_preference(gemspec)] + end + + # @param gemspec [Gem::Specification] + # @param out[IO, nil] output stream for logging + # + # @return [Array] + def fetch_dependencies gemspec, out: $stderr + # @param spec [Gem::Dependency] + only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| + Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" + dep = Gem.loaded_specs[spec.name] + # @todo is next line necessary? + 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 in RubyGems." + end.to_a + end + + private + + # True if the workspace has a root Gemfile. + # + # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler) + # + def gemfile? + directory && File.file?(File.join(directory, 'Gemfile')) + end + + # @return [Hash{String => Gem::Specification}] + def preference_map + @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] } + end + + # @param gemspec [Gem::Specification] + # @return [Gem::Specification] + def gemspec_or_preference gemspec + return gemspec unless preference_map.key?(gemspec.name) + return gemspec if gemspec.version == preference_map[gemspec.name].version + + # @todo this code is unused but broken + # @sg-ignore Unresolved call to by_path + change_gemspec_version gemspec, preference_map[by_path.name].version + end + + # @param gemspec [Gem::Specification] + # @param version [Gem::Version] + # @return [Gem::Specification] + def change_gemspec_version gemspec, version + Gem::Specification.find_by_name(gemspec.name, "= #{version}") + rescue Gem::MissingSpecError + Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead" + gemspec + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def only_runtime_dependencies gemspec + gemspec.dependencies - gemspec.development_dependencies + end + + # @return [Array] + def gemspecs_required_from_bundler + # @todo Handle projects with custom Bundler/Gemfile setups + return unless gemfile? + + if gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(directory) + # Find only the gems bundler is now using + Bundler.definition.locked_gems.specs.flat_map do |lazy_spec| + logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}" + [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)] + rescue Gem::MissingSpecError => e + logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with " \ + 'find_by_name, falling back to guess') + # can happen in local filesystem references + specs = resolve_require lazy_spec.name + logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? + next specs + end.compact + else + logger.info 'Fetching gemspecs required from Bundler (bundler/require)' + gemspecs_required_from_external_bundle + end + end + + # @return [Array] + def gemspecs_required_from_external_bundle + logger.info 'Fetching gemspecs required from external bundle' + return [] unless directory + + Solargraph.with_clean_env do + cmd = [ + 'ruby', '-e', + "require 'bundler'; " \ + "require 'json'; " \ + "Dir.chdir('#{directory}') { " \ + 'puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }' \ + '.to_h.to_json }' + ] + o, e, s = Open3.capture3(*cmd) + if s.success? + Solargraph.logger.debug "External bundle: #{o}" + hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} + hash.flat_map do |name, version| + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError => e + logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") + # can happen in local filesystem references + specs = resolve_require name + logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? + next specs + end.compact + else + Solargraph.logger.warn "Failed to load gems from bundle at #{directory}: #{e}" + end + end + end + end + end +end diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b03e573f0..e8c2c9763 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -8,15 +8,17 @@ Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) end + let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + it 'generates pins from gems' do - doc_map = Solargraph::DocMap.new(['ast'], []) + doc_map = Solargraph::DocMap.new(['ast'], workspace) 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 it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], []) + doc_map = Solargraph::DocMap.new(['not_a_gem'], workspace) expect(doc_map.unresolved_requires).to include('not_a_gem') end @@ -26,15 +28,14 @@ spec.version = '1.0.0' end allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], [gemspec]) + doc_map = Solargraph::DocMap.new(['not_a_gem'], workspace) 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 - workspace = Solargraph::Workspace.new(Dir.pwd) - plain_doc_map = Solargraph::DocMap.new([], [], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], [], workspace) + plain_doc_map = Solargraph::DocMap.new([], workspace) + doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], workspace) expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive end @@ -43,19 +44,19 @@ # 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).with(/path set/) - Solargraph::DocMap.new(['set'], []) + Solargraph::DocMap.new(['set'], workspace) end it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + expect { Solargraph::DocMap.new([nil], workspace) }.not_to raise_error end it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + expect { Solargraph::DocMap.new([''], workspace) }.not_to raise_error end it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) + doc_map = Solargraph::DocMap.new(['rspec'], workspace) expect(doc_map.dependencies.map(&:name)).to include('rspec-core') end @@ -70,7 +71,7 @@ def global(doc_map) Solargraph::Convention.register dummy_convention - doc_map = Solargraph::DocMap.new(['original_gem'], []) + doc_map = Solargraph::DocMap.new(['original_gem'], workspace) expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') diff --git a/spec/workspace_spec.rb b/spec/workspace_spec.rb index 572c3e131..dd8d8844b 100644 --- a/spec/workspace_spec.rb +++ b/spec/workspace_spec.rb @@ -72,7 +72,7 @@ gemspec_file = File.join(dir_path, 'test.gemspec') File.write(gemspec_file, '') expect(workspace.gemspec?).to be(true) - expect(workspace.gemspecs).to eq([gemspec_file]) + expect(workspace.gemspec_files).to eq([gemspec_file]) end it "generates default require path" do @@ -130,7 +130,7 @@ it 'ignores gemspecs in excluded directories' do # vendor/**/* is excluded by default workspace = Solargraph::Workspace.new('spec/fixtures/vendored') - expect(workspace.gemspecs).to be_empty + expect(workspace.gemspec_files).to be_empty end it 'rescues errors loading files into sources' do From 74e1baf9a7cacad85625eee28875281835ce8fb3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 12:56:26 -0400 Subject: [PATCH 105/524] Add @sg-ignore --- lib/solargraph/workspace/gemspecs.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 717c723da..c42b2d843 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -161,6 +161,7 @@ def gemspecs_required_from_external_bundle 'puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }' \ '.to_h.to_json }' ] + # @sg-ignore Unresolved call to capture3 on Module o, e, s = Open3.capture3(*cmd) if s.success? Solargraph.logger.debug "External bundle: #{o}" From 947d5b9c01b82d74c9f39613e0925ba975f0a98d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 16:42:36 -0400 Subject: [PATCH 106/524] Update rubocop todo file --- .rubocop_todo.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0ed335f34..95d352ebc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -64,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -82,7 +81,6 @@ Layout/ElseAlignment: # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' @@ -167,7 +165,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -1181,7 +1178,7 @@ Style/SafeNavigation: # Configuration parameters: Max. Style/SafeNavigationChainLength: Exclude: - - 'lib/solargraph/doc_map.rb' + - 'lib/solargraph/workspace/gemspecs.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: From c1e4c8d953cc512b28aa0477efd6005f82082330 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:08:28 -0400 Subject: [PATCH 107/524] Add some new specs --- spec/spec_helper.rb | 26 ++ .../gemspecs_fetch_dependencies_spec.rb | 88 ++++++ .../gemspecs_resolve_require_spec.rb | 295 ++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 spec/workspace/gemspecs_fetch_dependencies_spec.rb create mode 100644 spec/workspace/gemspecs_resolve_require_spec.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..59d107aa3 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,3 +43,29 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb new file mode 100644 index 000000000..21fe040e5 --- /dev/null +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +xdescribe Solargraph::Workspace::Gemspecs, '#fetch_dependencies' do + subject(:deps) { gemspecs.fetch_dependencies(gemspec) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:dir_path) { Dir.pwd } + + context 'when in our bundle' do + context 'with a Bundler::LazySpecification' do + let(:gemspec) do + Bundler::LazySpecification.new('solargraph', nil, nil) + end + + it 'finds a known dependency' do + pending('https://github.com/castwide/solargraph/pull/1006') + expect(deps.map(&:name)).to include('backport') + end + end + + context 'with gem whose dependency does not exist in our bundle' do + let(:gemspec) do + instance_double(Gem::Specification, + dependencies: [Gem::Dependency.new('activerecord')], + development_dependencies: [], + name: 'my_fake_gem', + version: '123') + end + let(:gem_name) { 'my_fake_gem' } + + it 'gives a useful message' do + pending('https://github.com/castwide/solargraph/pull/1006') + + output = capture_both { deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + let(:gemspec) do + Bundler::LazySpecification.new(gem_name, nil, nil) + end + + before do + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem '#{gem_name}' + GEMFILE + + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + + # ensure Gemfile.lock exists + unless File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + end + + context 'with gem that exists in our bundle' do + let(:gem_name) { 'undercover' } + + it 'finds dependencies' do + expect(deps.map(&:name)).to include('ast') + end + end + + context 'with gem does not exist in our bundle' do + let(:gem_name) { 'activerecord' } + + it 'gives a useful message' do + dep_names = nil + output = capture_both { dep_names = deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end +end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb new file mode 100644 index 000000000..f6217271c --- /dev/null +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -0,0 +1,295 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#resolve_require' do + subject(:specs) { gemspecs.resolve_require(require) } + + let(:gemspecs) { described_class.new(dir_path) } + + def find_or_install gem_name, version + Gem::Specification.find_by_name(gem_name, version) + rescue Gem::LoadError + install_gem(gem_name, version) + end + + def add_bundle + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'backport' + GEMFILE + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + # ensure Gemfile.lock exists + return if File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + + def install_gem gem_name, version + Bundler.with_unbundled_env do + cmd = Gem::Commands::InstallCommand.new + cmd.handle_options [gem_name, '-v', version] + cmd.execute + rescue Gem::SystemExitException => e + raise unless e.exit_code == 0 + end + end + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with a known gem' do + let(:require) { 'solargraph' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with an unknown type from Bundler / RubyGems' do + let(:require) { 'solargraph' } + let(:specish_objects) { [double] } + + before do + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + def configure_bundler_spec stub_value + platform = Gem::Platform::RUBY + bundler_stub_spec = Bundler::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + specish_objects = [bundler_stub_spec] + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + # specish_objects = Bundler.definition.locked_gems.specs + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + allow(bundler_stub_spec).to receive(:respond_to?).with(:name).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:version).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:gem_dir).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:materialize_for_installation).and_return(false) + allow(bundler_stub_spec).to receive_messages(name: 'solargraph', stub: stub_value) + end + + context 'with a Bundler::StubSpecification from Bundler / RubyGems' do + # this can happen from local gems, which is hard to test + # organically + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + + before do + platform = Gem::Platform::RUBY + real_spec = instance_double(Gem::Specification) + allow(real_spec).to receive(:name).and_return('solargraph') + gem_stub_spec = Gem::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + configure_bundler_spec(gem_stub_spec) + allow(gem_stub_spec).to receive_messages(name: 'solargraph', version: '123', spec: real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a Bundler::StubSpecification that resolves straight to Gem::Specification' do + # have seen different behavior with different versions of rubygems/bundler + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + let(:real_spec) { Gem::Specification.new('solargraph', '123') } + + before do + configure_bundler_spec(real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a less usual require mapping' do + let(:require) { 'diff/lcs' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['diff-lcs']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'returns the gemspec gem' do + expect(specs.map(&:name)).to include('solargraph') + end + end + end + + context 'with nil as directory' do + let(:dir_path) { nil } + + context 'with simple require' do + let(:require) { 'solargraph' } + + it 'finds solargraph' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'finds nothing' do + pending('https://github.com/castwide/solargraph/pull/1006') + + expect(specs).to be_empty + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + context 'with no actual bundle' do + let(:require) { 'bundler/require' } + + it 'raises' do + pending('https://github.com/castwide/solargraph/pull/1006') + + expect { specs }.to raise_error(Solargraph::BundleNotFoundError) + end + end + + context 'with Gemfile and Bundler.require' do + before { add_bundle } + + let(:require) { 'bundler/require' } + + it 'does not raise' do + expect { specs }.not_to raise_error + end + + it 'returns gems' do + expect(specs.map(&:name)).to include('backport') + end + end + + context 'with Gemfile but an unknown gem' do + before { add_bundle } + + let(:require) { 'unknown_gemlaksdflkdf' } + + it 'returns nil' do + expect(specs).to be_nil + end + end + + context 'with a Gemfile and a gem preference' do + # find_or_install helper doesn't seem to work on older versions + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + before do + add_bundle + find_or_install('backport', '1.0.0') + Gem::Specification.find_by_name('backport', '= 1.0.0') + end + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '1.0.0' + end + ] + end + + it 'returns the preferred gemspec' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.0.0') + end + + context 'with a gem preference that does not exist' do + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '99.0.0' + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.2.0') + end + end + + context 'with a gem preference already set to the version we use' do + let(:version) { Gem::Specification.find_by_name('backport').version.to_s } + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = version + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq(version) + end + end + end + end + end +end From f32bb177d45cb62090b3c94f64b83f4ce4f7c4c2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:14:07 -0400 Subject: [PATCH 108/524] Convince GHA to run on branch of branch --- .github/workflows/linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b4ef26bfe..4520280dc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -8,7 +8,7 @@ name: Linting on: workflow_dispatch: {} pull_request: - branches: [ master ] + branches: [ * ] push: branches: - 'main' From b1a736d1bb03aa4f51d4eb01553e869fd227b83f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:14:48 -0400 Subject: [PATCH 109/524] Convince GHA to run on branch of branch --- .github/workflows/linting.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 4520280dc..1078f5534 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -7,8 +7,7 @@ name: Linting on: workflow_dispatch: {} - pull_request: - branches: [ * ] + pull_request: true push: branches: - 'main' From 87cb546927e73b7e525c5a1aa622c856ed3c084d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:17:58 -0400 Subject: [PATCH 110/524] Convince GHA to run on branch of branch --- .github/workflows/linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 1078f5534..84299cbb5 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -10,7 +10,7 @@ on: pull_request: true push: branches: - - 'main' + - '*' tags: - 'v*' From 99c8b34a7d248f088c4d67a8b48bb38f1c4690ee Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:19:18 -0400 Subject: [PATCH 111/524] Convince GHA to run on branch of branch --- .github/workflows/linting.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 84299cbb5..aaefe0c16 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -7,10 +7,12 @@ name: Linting on: workflow_dispatch: {} - pull_request: true - push: + pull_request: branches: - '*' + push: + branches: + - 'main' tags: - 'v*' From b9a16c41b41385c49ed3da101af3a1fce7dd332b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:20:34 -0400 Subject: [PATCH 112/524] Convince GHA to run on branch of branch --- .github/workflows/plugins.yml | 3 ++- .github/workflows/rspec.yml | 3 ++- .github/workflows/typecheck.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b0f22ec3e..69f93e25c 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -4,7 +4,8 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: + - '*' permissions: contents: read diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..1ad29dc63 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -11,7 +11,8 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: + - '*' permissions: contents: read diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 0ae8a3d8a..26eb75a17 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -11,7 +11,8 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: + - '*' permissions: contents: read From 76e37a545ec479acbc7b1e005b0365a0fae7f285 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:30:26 -0400 Subject: [PATCH 113/524] Mark another new spec as pending --- spec/workspace/gemspecs_resolve_require_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index f6217271c..cda6c3d5b 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -243,6 +243,8 @@ def configure_bundler_spec stub_value end it 'returns the preferred gemspec' do + pending('https://github.com/castwide/solargraph/pull/1006') + gemspecs = described_class.new(dir_path, preferences: preferences) specs = gemspecs.resolve_require('backport') backport = specs.find { |spec| spec.name == 'backport' } From e08b61bf97c86b51e92da687de16a3ad7dfdb1b9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:31:12 -0400 Subject: [PATCH 114/524] Mark another new spec as pending --- spec/workspace/gemspecs_resolve_require_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index cda6c3d5b..2807c3384 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -263,6 +263,8 @@ def configure_bundler_spec stub_value end it 'returns the gemspec we do have' do + pending('https://github.com/castwide/solargraph/pull/1006') + gemspecs = described_class.new(dir_path, preferences: preferences) specs = gemspecs.resolve_require('backport') backport = specs.find { |spec| spec.name == 'backport' } From 33b9cb1241ab20fcc552e5f775ff4b16230cb86a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 09:33:38 -0400 Subject: [PATCH 115/524] Show missing coverage --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 1ad29dc63..a24b81335 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -70,4 +70,4 @@ jobs: run: bundle exec rake spec - name: Check PR coverage run: bundle exec rake undercover - continue-on-error: true + # continue-on-error: true TODO: Restore before merging From 33a185788e2781b75ee314d19c36c990e4fd8863 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 10:13:24 -0400 Subject: [PATCH 116/524] Handle some missed coverage areas --- lib/solargraph/api_map.rb | 10 +++++----- spec/api_map_method_spec.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index c2e0967f7..ce8f9a0c2 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -124,7 +124,7 @@ def doc_map # @return [::Array] def uncached_gemspecs - @doc_map&.uncached_gemspecs || [] + doc_map.uncached_gemspecs || [] end # @return [::Array] @@ -194,7 +194,7 @@ def self.load directory # @param out [IO, nil] # @return [void] def cache_all!(out) - @doc_map.cache_all!(out) + doc_map.cache_all!(out) end # @param gemspec [Gem::Specification] @@ -202,7 +202,7 @@ def cache_all!(out) # @param out [IO, nil] # @return [void] def cache_gem(gemspec, rebuild: false, out: nil) - @doc_map.cache(gemspec, rebuild: rebuild, out: out) + doc_map.cache(gemspec, rebuild: rebuild, out: out) end class << self @@ -700,9 +700,9 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] GemPins.combine_method_pins_by_path(with_resolved_aliases) end - # @return [Workspace, nil] + # @return [Workspace] def workspace - @doc_map&.workspace + doc_map.workspace end # @param fq_reference_tag [String] A fully qualified whose method should be pulled in diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index 9d4e4f553..d3b91321b 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -133,6 +133,37 @@ class B end end + describe '#cache_all!' do + it 'can cache gems without a bench' do + api_map = Solargraph::ApiMap.new + doc_map = instance_double('DocMap', cache_all!: true) + allow(Solargraph::DocMap).to receive(:new).and_return(doc_map) + api_map.cache_all!($stderr) + expect(doc_map).to have_received(:cache_all!).with($stderr) + end + end + + describe '#cache_gem' do + it 'can cache gem without a bench' do + api_map = Solargraph::ApiMap.new + expect { api_map.cache_gem('rake', out: StringIO.new) }.not_to raise_error + end + end + + describe '#cache_gem' do + it 'can get a default workspace without a bench' do + api_map = Solargraph::ApiMap.new + expect(api_map.workspace).not_to be_nil + end + end + + describe '#uncached_gemspecs' do + it 'can get uncached gemspecs workspace without a bench' do + api_map = Solargraph::ApiMap.new + expect(api_map.uncached_gemspecs).not_to be_nil + end + end + describe '#get_methods' do it 'recognizes mixin references from context' do source = Solargraph::Source.load_string(%( From 7c215108324ae9d5d625793ee413937e5e439a43 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 10:21:54 -0400 Subject: [PATCH 117/524] Clean up RSpec/MessageSpies rubocop violations --- .rubocop_todo.yml | 5 ----- spec/doc_map_spec.rb | 3 ++- spec/language_server/host/diagnoser_spec.rb | 3 ++- spec/language_server/host/message_worker_spec.rb | 3 ++- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b8d123824..5739f5fcd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1106,11 +1106,6 @@ RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: . -# SupportedStyles: have_received, receive -RSpec/MessageSpies: - EnforcedStyle: receive - RSpec/MissingExampleGroupArgument: Exclude: - 'spec/diagnostics/rubocop_helpers_spec.rb' diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b03e573f0..38fd8e5c5 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -42,8 +42,9 @@ 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).with(/path set/) + allow(Solargraph.logger).to receive(:warn) Solargraph::DocMap.new(['set'], []) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end it 'ignores nil requires' do diff --git a/spec/language_server/host/diagnoser_spec.rb b/spec/language_server/host/diagnoser_spec.rb index d59a843f1..697d352bd 100644 --- a/spec/language_server/host/diagnoser_spec.rb +++ b/spec/language_server/host/diagnoser_spec.rb @@ -1,9 +1,10 @@ describe Solargraph::LanguageServer::Host::Diagnoser do it "diagnoses on ticks" do host = double(Solargraph::LanguageServer::Host, options: { 'diagnostics' => true }, synchronizing?: false) + allow(host).to receive(:diagnose) diagnoser = Solargraph::LanguageServer::Host::Diagnoser.new(host) diagnoser.schedule 'file.rb' - expect(host).to receive(:diagnose).with('file.rb') diagnoser.tick + expect(host).to have_received(:diagnose).with('file.rb') end end diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index b9ce2a41f..526d88a07 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,11 +2,12 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - expect(host).to receive(:receive).with(message).and_return(nil) + allow(host).to receive(:receive) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick + expect(host).to have_received(:receive).with(message).and_return(nil) end end From 5afe71b7d740cff980f4af051f702c08b254ed3b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 10:43:02 -0400 Subject: [PATCH 118/524] Fix up allow syntax --- spec/language_server/host/message_worker_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/language_server/host/message_worker_spec.rb b/spec/language_server/host/message_worker_spec.rb index 526d88a07..5e5bef481 100644 --- a/spec/language_server/host/message_worker_spec.rb +++ b/spec/language_server/host/message_worker_spec.rb @@ -2,12 +2,12 @@ it "handle requests on queue" do host = double(Solargraph::LanguageServer::Host) message = {'method' => '$/example'} - allow(host).to receive(:receive) + allow(host).to receive(:receive).with(message).and_return(nil) worker = Solargraph::LanguageServer::Host::MessageWorker.new(host) worker.queue(message) expect(worker.messages).to eq [message] worker.tick - expect(host).to have_received(:receive).with(message).and_return(nil) + expect(host).to have_received(:receive).with(message) end end From aaa757347cb7218add6156d4484dde044d9bf8d9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 10:49:00 -0400 Subject: [PATCH 119/524] Allow solargraph version to be overidden for test purposes Useful for: 1. Maintaining a separate pin cache per branch to avoid contamination and get more accurate spec results locally 2. Testing solargraph-rails against branches consolidating multiple open PRs (e.g., the dated ones at https://github.com/apiology/solargraph/pulls) Includes a direnv implementation for #1 --- .envrc | 3 +++ lib/solargraph/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..815cb00d0 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# current git branch +SOLARGRAPH_FORCE_VERSION=0.0.0.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr '/' '-' | tr '_' '-') +export SOLARGRAPH_FORCE_VERSION diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index 94cc1b851..00ab4af98 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = '0.56.2' + VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.56.2') end From c4f92467787ef76e8464c4e17d9b4fc458e88100 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 14:09:08 -0400 Subject: [PATCH 120/524] Exercise log statements too --- spec/spec_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00cc6c8c3..a74b8f97e 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,6 +27,9 @@ end require 'solargraph' # Suppress logger output in specs (if possible) +# execute any logging blocks to make sure they don't blow up +Solargraph::Logging.logger.sev_threshold = Logger::DEBUG +# ...but still suppress logger output in specs (if possible) if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') Solargraph::Logging.logger.reopen(File::NULL) end From 54334790f6846289d2324714af3900221738c53c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 18:35:16 -0400 Subject: [PATCH 121/524] Better versioning in example --- .envrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envrc b/.envrc index 815cb00d0..92f925b17 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,3 @@ # current git branch -SOLARGRAPH_FORCE_VERSION=0.0.0.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr '/' '-' | tr '_' '-') +SOLARGRAPH_FORCE_VERSION=0.0.1.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr -d '/' | tr -d '-'| tr -d '_') export SOLARGRAPH_FORCE_VERSION From e38a79ad06d4a23fdb324390f2020dac0f2b4d50 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 5 Sep 2025 19:35:16 -0400 Subject: [PATCH 122/524] Backport in some specs --- spec/spec_helper.rb | 26 ++ .../gemspecs_fetch_dependencies_spec.rb | 95 ++++++ .../gemspecs_resolve_require_spec.rb | 299 ++++++++++++++++++ 3 files changed, 420 insertions(+) create mode 100644 spec/workspace/gemspecs_fetch_dependencies_spec.rb create mode 100644 spec/workspace/gemspecs_resolve_require_spec.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a74b8f97e..366c22cc3 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -46,3 +46,29 @@ def with_env_var(name, value) ENV[name] = old_value # Restore the old value end end + +def capture_stdout &block + original_stdout = $stdout + $stdout = StringIO.new + begin + block.call + $stdout.string + ensure + $stdout = original_stdout + end +end + +def capture_both &block + original_stdout = $stdout + original_stderr = $stderr + stringio = StringIO.new + $stdout = stringio + $stderr = stringio + begin + block.call + ensure + $stdout = original_stdout + $stderr = original_stderr + end + stringio.string +end diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb new file mode 100644 index 000000000..d466fc0d7 --- /dev/null +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#fetch_dependencies' do + subject(:deps) { gemspecs.fetch_dependencies(gemspec) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:dir_path) { Dir.pwd } + + context 'when in our bundle' do + xcontext 'with a Bundler::LazySpecification' do + let(:gemspec) do + Bundler::LazySpecification.new('solargraph', nil, nil) + end + + it 'finds a known dependency' do + pending('https://github.com/castwide/solargraph/pull/1006') + expect(deps.map(&:name)).to include('backport') + end + end + + context 'with gem whose dependency does not exist in our bundle' do + let(:gemspec) do + instance_double(Gem::Specification, + dependencies: [Gem::Dependency.new('activerecord')], + development_dependencies: [], + name: 'my_fake_gem', + version: '123') + end + let(:gem_name) { 'my_fake_gem' } + + it 'gives a useful message' do + pending('https://github.com/castwide/solargraph/pull/1006') + + output = capture_both { deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + let(:gemspec) do + Gem::Specification.find_by_name(gem_name) + end + + before do + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem '#{gem_name}' + GEMFILE + + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + + # ensure Gemfile.lock exists + unless File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + end + + context 'with gem that exists in our bundle' do + let(:gem_name) { 'undercover' } + + it 'finds dependencies' do + expect(deps.map(&:name)).to include('ast') + end + end + + context 'with gem does not exist in our bundle' do + let(:gemspec) do + Gem::Specification.new(fake_gem_name) + end + + let(:gem_name) { 'undercover' } + + let(:fake_gem_name) { 'faaaaaake912' } + + it 'gives a useful message' do + pending('https://github.com/castwide/solargraph/pull/1006') + dep_names = nil + output = capture_both { dep_names = deps.map(&:name) } + expect(output).to include('Please install the gem activerecord') + end + end + end +end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb new file mode 100644 index 000000000..2807c3384 --- /dev/null +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#resolve_require' do + subject(:specs) { gemspecs.resolve_require(require) } + + let(:gemspecs) { described_class.new(dir_path) } + + def find_or_install gem_name, version + Gem::Specification.find_by_name(gem_name, version) + rescue Gem::LoadError + install_gem(gem_name, version) + end + + def add_bundle + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'backport' + GEMFILE + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + # ensure Gemfile.lock exists + return if File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + + def install_gem gem_name, version + Bundler.with_unbundled_env do + cmd = Gem::Commands::InstallCommand.new + cmd.handle_options [gem_name, '-v', version] + cmd.execute + rescue Gem::SystemExitException => e + raise unless e.exit_code == 0 + end + end + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with a known gem' do + let(:require) { 'solargraph' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with an unknown type from Bundler / RubyGems' do + let(:require) { 'solargraph' } + let(:specish_objects) { [double] } + + before do + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + def configure_bundler_spec stub_value + platform = Gem::Platform::RUBY + bundler_stub_spec = Bundler::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + specish_objects = [bundler_stub_spec] + lockfile = instance_double(Pathname) + locked_gems = instance_double(Bundler::LockfileParser, specs: specish_objects) + definition = instance_double(Bundler::Definition, + locked_gems: locked_gems, + lockfile: lockfile) + # specish_objects = Bundler.definition.locked_gems.specs + allow(Bundler).to receive(:definition).and_return(definition) + allow(lockfile).to receive(:to_s).and_return(dir_path) + allow(bundler_stub_spec).to receive(:respond_to?).with(:name).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:version).and_return(true) + allow(bundler_stub_spec).to receive(:respond_to?).with(:gem_dir).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:materialize_for_installation).and_return(false) + allow(bundler_stub_spec).to receive_messages(name: 'solargraph', stub: stub_value) + end + + context 'with a Bundler::StubSpecification from Bundler / RubyGems' do + # this can happen from local gems, which is hard to test + # organically + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + + before do + platform = Gem::Platform::RUBY + real_spec = instance_double(Gem::Specification) + allow(real_spec).to receive(:name).and_return('solargraph') + gem_stub_spec = Gem::StubSpecification.new('solargraph', '123', platform, spec_fetcher) + configure_bundler_spec(gem_stub_spec) + allow(gem_stub_spec).to receive_messages(name: 'solargraph', version: '123', spec: real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a Bundler::StubSpecification that resolves straight to Gem::Specification' do + # have seen different behavior with different versions of rubygems/bundler + + let(:require) { 'solargraph' } + let(:spec_fetcher) { instance_double(Gem::SpecFetcher) } + let(:real_spec) { Gem::Specification.new('solargraph', '123') } + + before do + configure_bundler_spec(real_spec) + end + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with a less usual require mapping' do + let(:require) { 'diff/lcs' } + + it 'returns a single spec' do + expect(specs.size).to eq(1) + end + + it 'resolves to the right known gem' do + expect(specs.map(&:name)).to eq(['diff-lcs']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'returns the gemspec gem' do + expect(specs.map(&:name)).to include('solargraph') + end + end + end + + context 'with nil as directory' do + let(:dir_path) { nil } + + context 'with simple require' do + let(:require) { 'solargraph' } + + it 'finds solargraph' do + expect(specs.map(&:name)).to eq(['solargraph']) + end + end + + context 'with Bundler.require' do + let(:require) { 'bundler/require' } + + it 'finds nothing' do + pending('https://github.com/castwide/solargraph/pull/1006') + + expect(specs).to be_empty + end + end + end + + context 'with external bundle' do + let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + + context 'with no actual bundle' do + let(:require) { 'bundler/require' } + + it 'raises' do + pending('https://github.com/castwide/solargraph/pull/1006') + + expect { specs }.to raise_error(Solargraph::BundleNotFoundError) + end + end + + context 'with Gemfile and Bundler.require' do + before { add_bundle } + + let(:require) { 'bundler/require' } + + it 'does not raise' do + expect { specs }.not_to raise_error + end + + it 'returns gems' do + expect(specs.map(&:name)).to include('backport') + end + end + + context 'with Gemfile but an unknown gem' do + before { add_bundle } + + let(:require) { 'unknown_gemlaksdflkdf' } + + it 'returns nil' do + expect(specs).to be_nil + end + end + + context 'with a Gemfile and a gem preference' do + # find_or_install helper doesn't seem to work on older versions + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + before do + add_bundle + find_or_install('backport', '1.0.0') + Gem::Specification.find_by_name('backport', '= 1.0.0') + end + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '1.0.0' + end + ] + end + + it 'returns the preferred gemspec' do + pending('https://github.com/castwide/solargraph/pull/1006') + + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.0.0') + end + + context 'with a gem preference that does not exist' do + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = '99.0.0' + end + ] + end + + it 'returns the gemspec we do have' do + pending('https://github.com/castwide/solargraph/pull/1006') + + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq('1.2.0') + end + end + + context 'with a gem preference already set to the version we use' do + let(:version) { Gem::Specification.find_by_name('backport').version.to_s } + + let(:preferences) do + [ + Gem::Specification.new.tap do |spec| + spec.name = 'backport' + spec.version = version + end + ] + end + + it 'returns the gemspec we do have' do + gemspecs = described_class.new(dir_path, preferences: preferences) + specs = gemspecs.resolve_require('backport') + backport = specs.find { |spec| spec.name == 'backport' } + + expect(backport.version.to_s).to eq(version) + end + end + end + end + end +end From bb0f6079024f1e28ee9f12c26bbc626fe475d2ce Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 14:18:16 -0400 Subject: [PATCH 123/524] Rerun rubocop todo --- .rubocop_todo.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0ed335f34..c55a29039 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -64,7 +64,6 @@ Layout/BlockAlignment: Layout/ClosingHeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment. @@ -167,7 +166,6 @@ Layout/HashAlignment: Layout/HeredocIndentation: Exclude: - 'spec/diagnostics/rubocop_spec.rb' - - 'spec/rbs_map/conversions_spec.rb' - 'spec/yard_map/mapper/to_method_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -685,10 +683,13 @@ RSpec/LetBeforeExamples: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: . +# Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: - EnforcedStyle: receive + Exclude: + - 'spec/doc_map_spec.rb' + - 'spec/language_server/host/diagnoser_spec.rb' + - 'spec/language_server/host/message_worker_spec.rb' RSpec/MissingExampleGroupArgument: Exclude: @@ -750,10 +751,6 @@ RSpec/ScatteredLet: Exclude: - 'spec/complex_type_spec.rb' -# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata. -RSpec/SpecFilePathFormat: - Enabled: false - RSpec/StubbedMock: Exclude: - 'spec/language_server/host/message_worker_spec.rb' From 7edc8691344bec6ca09fe1b8a4cce69df3cb0113 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 22:04:57 -0400 Subject: [PATCH 124/524] Reodo rubocop todo --- .rubocop_todo.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c55a29039..c2fe01517 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -223,11 +223,6 @@ Layout/SpaceAfterComma: Layout/SpaceAroundEqualsInParameterDefault: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAroundKeyword: - Exclude: - - 'spec/rbs_map/conversions_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. # SupportedStylesForExponentOperator: space, no_space @@ -643,7 +638,6 @@ RSpec/ExampleWording: # This cop supports safe autocorrection (--autocorrect). RSpec/ExcessiveDocstringSpacing: Exclude: - - 'spec/rbs_map/conversions_spec.rb' - 'spec/source/chain/call_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -659,21 +653,10 @@ RSpec/ExpectActual: RSpec/HookArgument: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: . -# SupportedStyles: is_expected, should -RSpec/ImplicitExpect: - EnforcedStyle: should - # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -RSpec/LeadingSubject: - Exclude: - - 'spec/rbs_map/conversions_spec.rb' - RSpec/LeakyConstantDeclaration: Exclude: - 'spec/complex_type_spec.rb' @@ -974,7 +957,6 @@ Style/MapIntoArray: Exclude: - 'lib/solargraph/diagnostics/update_errors.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - - 'lib/solargraph/type_checker/param_def.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: @@ -1048,7 +1030,6 @@ Style/Next: - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - 'lib/solargraph/pin/signature.rb' - 'lib/solargraph/source_map/clip.rb' - - 'lib/solargraph/type_checker/checks.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. @@ -1295,10 +1276,7 @@ YARD/MismatchName: Enabled: false YARD/TagTypeSyntax: - Exclude: - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. From 86965f26b813efdd98f87a22cc90faef4871a968 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 22:15:50 -0400 Subject: [PATCH 125/524] Fix merge --- lib/solargraph/api_map/store.rb | 2 +- spec/rbs_map/conversions_spec.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index e12431c94..e3972415c 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -297,7 +297,7 @@ def include_reference_pins index.include_reference_pins end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references index.prepend_references end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index ea7d3df02..30f784673 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -135,7 +135,6 @@ class C < ::B::C param.name == 'chdir' end expect(chdir_param).not_to be_nil, -> { "Found pin #{method_pin.to_rbs} from #{method_pin.type_location}" } ->>>>>>> origin/master end end end From 557edd0574bc2a1dbc0f87937f64e7a019c4a4bc Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 23:04:04 -0400 Subject: [PATCH 126/524] Fix merge --- spec/rbs_map/conversions_spec.rb | 51 ++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 30f784673..7eac07209 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -25,6 +25,33 @@ attr_reader :temp_dir + + context 'with overlapping module hierarchies and inheritance' do + let(:rbs) do + <<~RBS + module B + class C + def foo: () -> String + end + end + module A + module B + class C < ::B::C + end + end + end + RBS + end + + subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } + + before do + api_map.index conversions.pins + end + + it { should be_a(Solargraph::Pin::Method) } + end + context 'with untyped response' do let(:rbs) do <<~RBS @@ -51,7 +78,7 @@ def bar: () -> untyped @api_map = Solargraph::ApiMap.load_with_cache('.') end - let(:api_map) { @api_map } # rubocop:disable RSpec/InstanceVariable + let(:api_map) { @api_map } context 'with superclass pin for Parser::AST::Node' do let(:superclass_pin) do @@ -97,28 +124,6 @@ class Sub < Hash[Symbol, untyped] .uniq).to eq(['Symbol']) end end - - context 'with overlapping module hierarchies and inheritance' do - let(:rbs) do - <<~RBS - module B - class C - def foo: () -> String - end - end - module A - module B - class C < ::B::C - end - end - end - RBS - end - - subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } - - it { should be_a(Solargraph::Pin::Method) } - end end if Gem::Version.new(RBS::VERSION) >= Gem::Version.new('3.9.1') From 9628ef4be67f0ff5b3769760156bacd4673c83c7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 23:09:23 -0400 Subject: [PATCH 127/524] Clean up spec --- spec/rbs_map/conversions_spec.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 7eac07209..e964e69f2 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -25,8 +25,9 @@ attr_reader :temp_dir - context 'with overlapping module hierarchies and inheritance' do + subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } + let(:rbs) do <<~RBS module B @@ -43,16 +44,16 @@ class C < ::B::C RBS end - subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } - before do api_map.index conversions.pins end - it { should be_a(Solargraph::Pin::Method) } + it { is_expected.to be_a(Solargraph::Pin::Method) } end context 'with untyped response' do + subject(:method_pin) { conversions.pins.find { |pin| pin.path == 'Foo#bar' } } + let(:rbs) do <<~RBS class Foo @@ -61,13 +62,11 @@ def bar: () -> untyped RBS end - subject(:method_pin) { conversions.pins.find { |pin| pin.path == 'Foo#bar' } } - - it { should_not be_nil } + it { is_expected.not_to be_nil } - it { should be_a(Solargraph::Pin::Method) } + it { is_expected.to be_a(Solargraph::Pin::Method) } - it 'maps untyped in RBS to undefined in Solargraph 'do + it 'maps untyped in RBS to undefined in Solargraph' do expect(method_pin.return_type.tag).to eq('undefined') end end From 4bfee71fce385524654c955d1a22988ffea25189 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 23:15:18 -0400 Subject: [PATCH 128/524] Add @type annotation --- lib/solargraph/type_checker.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 0b43c44fe..c315a407f 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -481,6 +481,7 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, else ptype = data[:qualified] unless ptype.undefined? + # @type [ComplexType] argtype = argchain.infer(api_map, block_pin, locals) if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") From 2548d6d8bcf9dd3a4de3933eaebeeef355e7a275 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 05:36:42 -0400 Subject: [PATCH 129/524] Drop xcontext --- spec/workspace/gemspecs_fetch_dependencies_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb index d466fc0d7..285f8e1a0 100644 --- a/spec/workspace/gemspecs_fetch_dependencies_spec.rb +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -11,7 +11,7 @@ let(:dir_path) { Dir.pwd } context 'when in our bundle' do - xcontext 'with a Bundler::LazySpecification' do + context 'with a Bundler::LazySpecification' do let(:gemspec) do Bundler::LazySpecification.new('solargraph', nil, nil) end From e51777bc2b97e031e0911e3edc5461d1acd3758d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 05:43:30 -0400 Subject: [PATCH 130/524] Rename method and add comments --- lib/solargraph/workspace/gemspecs.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index c42b2d843..1f4fef27b 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -36,7 +36,12 @@ def initialize directory, preferences: [] # @return [::Array, nil] def resolve_require require return nil if require.empty? - return gemspecs_required_from_bundler if require == 'bundler/require' + + # This is added in the parser when it sees 'Bundler.require' - + # see https://bundler.io/guides/bundler_setup.html ' + # + # @todo handle different arguments to Bundler.require + return auto_required_gemspecs_from_bundler if require == 'bundler/require' # @sg-ignore Variable type could not be inferred for gemspec # @type [Gem::Specification, nil] @@ -85,7 +90,7 @@ def fetch_dependencies gemspec, out: $stderr # True if the workspace has a root Gemfile. # - # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler) + # @todo Handle projects with custom Bundler/Gemfile setups (see #auto_required_gemspecs_from_bundler) # def gemfile? directory && File.file?(File.join(directory, 'Gemfile')) @@ -124,7 +129,7 @@ def only_runtime_dependencies gemspec end # @return [Array] - def gemspecs_required_from_bundler + def auto_required_gemspecs_from_bundler # @todo Handle projects with custom Bundler/Gemfile setups return unless gemfile? From 837d7f67872f47619200320bafb7d80da9abad95 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 16:57:05 -0400 Subject: [PATCH 131/524] Force build --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..bfa6dce07 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,8 +48,8 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version - - name: Run tests - run: bundle exec rake spec +# - name: Run tests +# run: bundle exec rake spec undercover: runs-on: ubuntu-latest steps: From a4208e71241a3b2e5e2f9198d8d135a065bd7ea4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:10:41 -0400 Subject: [PATCH 132/524] Restore --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index bfa6dce07..ecc3d9771 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,8 +48,8 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version -# - name: Run tests -# run: bundle exec rake spec + - name: Run tests + run: bundle exec rake spec undercover: runs-on: ubuntu-latest steps: From b66f2ac85a8d5599e36decc6a24fce36eed2f382 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:14:22 -0400 Subject: [PATCH 133/524] install -> update with rbs collection --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 730882e30..b013abd3b 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -164,7 +164,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection install + bundle exec --gemfile ../../Gemfile rbs collection update cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From a09a9af2bc797134fac776c370ce4fcc2c6db121 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:25:05 -0400 Subject: [PATCH 134/524] Try Ruby 3.2 --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b013abd3b..af9997846 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,7 +144,8 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - ruby-version: '3.0' + # RBS 3.9 supports Ruby 3.2+ + ruby-version: '3.2' bundler-cache: false bundler: latest env: @@ -164,7 +165,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection update + bundle exec --gemfile ../../Gemfile rbs collection install cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From 6fc8febcdd4cb9515785e0f85788497ad8923ae7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:44:39 -0400 Subject: [PATCH 135/524] Update solargraph --- .github/workflows/plugins.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index af9997846..ef9fe0155 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,8 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile - bundle install - bundle update rbs + bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From 388c170d76531530012eb1dff32579a059e3bda6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:45:56 -0400 Subject: [PATCH 136/524] Re-add bundle install --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index ef9fe0155..a97b27c7c 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,6 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile + bundle install bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR From f80b73a020dd405addf8f56d2317e31614c2f8da Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:58:16 -0400 Subject: [PATCH 137/524] Drop MATRIX_SOLARGRAPH_VERSION --- .github/workflows/plugins.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a97b27c7c..4dedbd93f 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -149,7 +149,6 @@ jobs: bundler-cache: false bundler: latest env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" - name: Install gems run: | @@ -170,7 +169,6 @@ jobs: # bundle exec rbs collection init # bundle exec rbs collection install env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" MATRIX_RAILS_MAJOR_VERSION: '7' - name: Run specs @@ -184,5 +182,4 @@ jobs: bundle info yard ALLOW_IMPROVEMENTS=true bundle exec rake spec env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" From ce2bee62f20628f33f1e9abb98f7faf96f69166f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 18:09:04 -0400 Subject: [PATCH 138/524] Drop debugging changes --- .github/workflows/plugins.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 4dedbd93f..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,8 +144,7 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - # RBS 3.9 supports Ruby 3.2+ - ruby-version: '3.2' + ruby-version: '3.0' bundler-cache: false bundler: latest env: @@ -158,7 +157,7 @@ jobs: cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile bundle install - bundle update solargraph rbs + bundle update rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From b2f33014d31f7d4b008845bb46006496a903df85 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 16:57:05 -0400 Subject: [PATCH 139/524] Force build --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..bfa6dce07 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,8 +48,8 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version - - name: Run tests - run: bundle exec rake spec +# - name: Run tests +# run: bundle exec rake spec undercover: runs-on: ubuntu-latest steps: From 8d0c4263b25c0930a09b742cd18c43fe269b8228 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:10:41 -0400 Subject: [PATCH 140/524] Restore --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index bfa6dce07..ecc3d9771 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -48,8 +48,8 @@ jobs: run: | bundle install bundle update rbs # use latest available for this Ruby version -# - name: Run tests -# run: bundle exec rake spec + - name: Run tests + run: bundle exec rake spec undercover: runs-on: ubuntu-latest steps: From cf54f57a9645ae0542b7ea1dede63dd8b85ff9af Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:14:22 -0400 Subject: [PATCH 141/524] install -> update with rbs collection --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 730882e30..b013abd3b 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -164,7 +164,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection install + bundle exec --gemfile ../../Gemfile rbs collection update cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From e2b034f0516ea84b1e95b8aa4d42fcf726f75a00 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:25:05 -0400 Subject: [PATCH 142/524] Try Ruby 3.2 --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b013abd3b..af9997846 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,7 +144,8 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - ruby-version: '3.0' + # RBS 3.9 supports Ruby 3.2+ + ruby-version: '3.2' bundler-cache: false bundler: latest env: @@ -164,7 +165,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection update + bundle exec --gemfile ../../Gemfile rbs collection install cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From 28cd8d9803e983caa361e7b95cba8e547e7f4ac8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:44:39 -0400 Subject: [PATCH 143/524] Update solargraph --- .github/workflows/plugins.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index af9997846..ef9fe0155 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,8 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile - bundle install - bundle update rbs + bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From e2d5c0946da81ccbd0cc0388bfc02004bc7b96c2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:45:56 -0400 Subject: [PATCH 144/524] Re-add bundle install --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index ef9fe0155..a97b27c7c 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,6 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile + bundle install bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR From 4036347bd6e42351284101f114e017b9a07f28a9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:58:16 -0400 Subject: [PATCH 145/524] Drop MATRIX_SOLARGRAPH_VERSION --- .github/workflows/plugins.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a97b27c7c..4dedbd93f 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -149,7 +149,6 @@ jobs: bundler-cache: false bundler: latest env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" - name: Install gems run: | @@ -170,7 +169,6 @@ jobs: # bundle exec rbs collection init # bundle exec rbs collection install env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" MATRIX_RAILS_MAJOR_VERSION: '7' - name: Run specs @@ -184,5 +182,4 @@ jobs: bundle info yard ALLOW_IMPROVEMENTS=true bundle exec rake spec env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" From 2031cbb20644803d515a11948416394432aeaa7c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 18:09:04 -0400 Subject: [PATCH 146/524] Drop debugging changes --- .github/workflows/plugins.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 4dedbd93f..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,8 +144,7 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - # RBS 3.9 supports Ruby 3.2+ - ruby-version: '3.2' + ruby-version: '3.0' bundler-cache: false bundler: latest env: @@ -158,7 +157,7 @@ jobs: cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile bundle install - bundle update solargraph rbs + bundle update rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From dbe9a3edc5291e6cf50632560893306ee074a79f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 18:40:51 -0400 Subject: [PATCH 147/524] Update expectations from master branch --- .rubocop_todo.yml | 11 ----------- spec/convention_spec.rb | 2 -- 2 files changed, 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c55a29039..89f703d23 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -280,7 +280,6 @@ Layout/TrailingWhitespace: Exclude: - 'lib/solargraph/language_server/message/client/register_capability.rb' - 'spec/api_map/config_spec.rb' - - 'spec/convention_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -338,11 +337,6 @@ Lint/DuplicateBranch: Lint/DuplicateMethods: Enabled: false -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/convention_spec.rb' - # Configuration parameters: AllowComments. Lint/EmptyClass: Enabled: false @@ -618,11 +612,6 @@ RSpec/DescribeClass: RSpec/DescribedClass: Enabled: false -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/convention_spec.rb' - # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterFinalLet: Exclude: diff --git a/spec/convention_spec.rb b/spec/convention_spec.rb index 98a8f41bf..b6f4fc52e 100644 --- a/spec/convention_spec.rb +++ b/spec/convention_spec.rb @@ -1,5 +1,4 @@ describe Solargraph::Convention do - # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'newly defined pins are resolved by ApiMap after file changes' do filename = 'test.rb' @@ -106,5 +105,4 @@ def local _source_map described_class.unregister updated_dummy_convention end - # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end From 217fecdcf8bf20402c78348a86aa9f4c083c1a6d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 18:40:51 -0400 Subject: [PATCH 148/524] Update expectations from master branch --- .rubocop_todo.yml | 11 ----------- spec/convention_spec.rb | 2 -- 2 files changed, 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3965777d9..1b3783d86 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -279,7 +279,6 @@ Layout/TrailingWhitespace: Exclude: - 'lib/solargraph/language_server/message/client/register_capability.rb' - 'spec/api_map/config_spec.rb' - - 'spec/convention_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -337,11 +336,6 @@ Lint/DuplicateBranch: Lint/DuplicateMethods: Enabled: false -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/convention_spec.rb' - # Configuration parameters: AllowComments. Lint/EmptyClass: Enabled: false @@ -609,11 +603,6 @@ RSpec/DescribeClass: RSpec/DescribedClass: Enabled: false -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/convention_spec.rb' - # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterFinalLet: Exclude: diff --git a/spec/convention_spec.rb b/spec/convention_spec.rb index 98a8f41bf..b6f4fc52e 100644 --- a/spec/convention_spec.rb +++ b/spec/convention_spec.rb @@ -1,5 +1,4 @@ describe Solargraph::Convention do - # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'newly defined pins are resolved by ApiMap after file changes' do filename = 'test.rb' @@ -106,5 +105,4 @@ def local _source_map described_class.unregister updated_dummy_convention end - # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end From 2d2f0a77d8d17b9dabeee2a65f3ed636e528437b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:14:22 -0400 Subject: [PATCH 149/524] install -> update with rbs collection --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 730882e30..b013abd3b 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -164,7 +164,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection install + bundle exec --gemfile ../../Gemfile rbs collection update cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From 3db4b1f3b049338fea2f871724c2b6a7d5de7277 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:25:05 -0400 Subject: [PATCH 150/524] Try Ruby 3.2 --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b013abd3b..af9997846 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,7 +144,8 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - ruby-version: '3.0' + # RBS 3.9 supports Ruby 3.2+ + ruby-version: '3.2' bundler-cache: false bundler: latest env: @@ -164,7 +165,7 @@ jobs: cd ${RAILS_DIR} bundle install bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection update + bundle exec --gemfile ../../Gemfile rbs collection install cd ../../ # bundle exec rbs collection init # bundle exec rbs collection install From faa64283a3e63ec203d94a8d76ff1de3b14bcd1f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:44:39 -0400 Subject: [PATCH 151/524] Update solargraph --- .github/workflows/plugins.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index af9997846..ef9fe0155 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,8 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile - bundle install - bundle update rbs + bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From 3f68a02944a1bd249a3ea6d2c5964dd0f467c798 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:45:56 -0400 Subject: [PATCH 152/524] Re-add bundle install --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index ef9fe0155..a97b27c7c 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -158,6 +158,7 @@ jobs: export BUNDLE_PATH cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile + bundle install bundle update solargraph rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR From b8c16054ec9be28fb3143113bd58424af82acdd6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 17:58:16 -0400 Subject: [PATCH 153/524] Drop MATRIX_SOLARGRAPH_VERSION --- .github/workflows/plugins.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a97b27c7c..4dedbd93f 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -149,7 +149,6 @@ jobs: bundler-cache: false bundler: latest env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" - name: Install gems run: | @@ -170,7 +169,6 @@ jobs: # bundle exec rbs collection init # bundle exec rbs collection install env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" MATRIX_RAILS_MAJOR_VERSION: '7' - name: Run specs @@ -184,5 +182,4 @@ jobs: bundle info yard ALLOW_IMPROVEMENTS=true bundle exec rake spec env: - MATRIX_SOLARGRAPH_VERSION: '>=0.56.0.pre1' MATRIX_RAILS_VERSION: "7.0" From d3a0c36a5ca735759a050e1ccc5114832e01ba36 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 18:09:04 -0400 Subject: [PATCH 154/524] Drop debugging changes --- .github/workflows/plugins.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 4dedbd93f..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -144,8 +144,7 @@ jobs: uses: ruby/setup-ruby@v1 with: # solargraph-rails supports Ruby 3.0+ - # RBS 3.9 supports Ruby 3.2+ - ruby-version: '3.2' + ruby-version: '3.0' bundler-cache: false bundler: latest env: @@ -158,7 +157,7 @@ jobs: cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile bundle install - bundle update solargraph rbs + bundle update rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} From ef3fc9d700c117e0e466a16bbcc469e6ca066481 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 8 Sep 2025 15:21:31 -0400 Subject: [PATCH 155/524] Add empty-directory-related regression spec --- .../message/text_document/definition_spec.rb | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/spec/language_server/message/text_document/definition_spec.rb b/spec/language_server/message/text_document/definition_spec.rb index 72ff77f1e..b6c98b99b 100644 --- a/spec/language_server/message/text_document/definition_spec.rb +++ b/spec/language_server/message/text_document/definition_spec.rb @@ -1,4 +1,33 @@ describe Solargraph::LanguageServer::Message::TextDocument::Definition do + it 'prepares empty directory' do + Dir.mktmpdir do |dir| + host = Solargraph::LanguageServer::Host.new + test_rb_path = File.join(dir, 'test.rb') + thing_rb_path = File.join(dir, 'thing.rb') + FileUtils.cp('spec/fixtures/workspace/lib/other.rb', test_rb_path) + FileUtils.cp('spec/fixtures/workspace/lib/thing.rb', thing_rb_path) + host.prepare(dir) + sleep 0.1 until host.libraries.all?(&:mapped?) + host.catalog + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(test_rb_path) + other_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(thing_rb_path) + message = Solargraph::LanguageServer::Message::TextDocument::Definition + .new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) + message.process + expect(message.result.first[:uri]).to eq(other_uri) + end + end + it 'finds definitions of methods' do host = Solargraph::LanguageServer::Host.new host.prepare('spec/fixtures/workspace') @@ -21,7 +50,7 @@ expect(message.result.first[:uri]).to eq(other_uri) end - it 'finds definitions of require paths' do + it 'finds definitions of require paths', time_limit_seconds: 120 do path = File.absolute_path('spec/fixtures/workspace') host = Solargraph::LanguageServer::Host.new host.prepare(path) From bcd5fab2e88c7baf5b4bc0802835ebfd01d41ca5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 8 Sep 2025 15:28:18 -0400 Subject: [PATCH 156/524] Factor out a find_gem() method --- lib/solargraph/shell.rb | 2 +- lib/solargraph/workspace.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..5c3fc0581 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -145,7 +145,7 @@ def gems *names STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems." else names.each do |name| - spec = Gem::Specification.find_by_name(*name.split('=')) + spec = api_map.workspace.find_gem(*name.split('=')) do_cache spec, api_map rescue Gem::MissingSpecError warn "Gem '#{name}' not found" diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index e2d3d7495..1531509af 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -162,6 +162,16 @@ def rbs_collection_config_path end end + # @param name [String] + # @param version [String, nil] + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError + nil + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] From 9b713b27c4f3ced6377b4b352fd249e2bda0ab59 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 9 Sep 2025 11:20:02 -0400 Subject: [PATCH 157/524] Fix combined gem cache implementation --- lib/solargraph/pin_cache.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 8c47cfef4..d48873c53 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -119,7 +119,8 @@ def uncache_gem gemspec, out: nil PinCache.uncache(yard_gem_path(gemspec), out: out) uncache_by_prefix(rbs_collection_pins_path_prefix(gemspec), out: out) uncache_by_prefix(combined_path_prefix(gemspec), out: out) - combined_pins_in_memory.delete([gemspec.name, gemspec.version]) + rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) + combined_pins_in_memory.delete([gemspec.name, gemspec.version, rbs_version_cache_key]) end # @param gemspec [Gem::Specification, Bundler::LazySpecification] @@ -380,7 +381,11 @@ def combined_gem? gemspec, hash # @param hash [String, nil] # @return [Array, nil] def load_combined_gem gemspec, hash - PinCache.load(combined_path(gemspec, hash)) + cached = combined_pins_in_memory[[gemspec.name, gemspec.version, hash]] + return cached if cached + loaded = PinCache.load(combined_path(gemspec, hash)) + combined_pins_in_memory[[gemspec.name, gemspec.version, hash]] = loaded if loaded + loaded end # @param gemspec [Gem::Specification] From 4e02576942b48d8320123c9921324db1c203ab26 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 9 Sep 2025 16:36:23 -0400 Subject: [PATCH 158/524] Improve doc_map_spec.rb --- spec/doc_map_spec.rb | 177 +++++++++++++++++++++++++++++++++---------- 1 file changed, 135 insertions(+), 42 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 1315f6c90..2624e472d 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -1,80 +1,173 @@ # frozen_string_literal: true +require 'bundler' +require 'benchmark' + describe Solargraph::DocMap do - before :all do - # We use ast here because it's a known dependency. - gemspec = Gem::Specification.find_by_name('ast') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + subject(:doc_map) do + described_class.new(requires, [], workspace) 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) + let(:out) { StringIO.new } + let(:pre_cache) { true } + let(:requires) { [] } + + let(:workspace) do + Solargraph::Workspace.new(Dir.pwd) end - it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], []) - expect(doc_map.unresolved_requires).to include('not_a_gem') + let(:plain_doc_map) { described_class.new([], [], workspace) } + + before do + doc_map.cache_all!(nil) if pre_cache end - it 'tracks uncached_gemspecs' do - gemspec = Gem::Specification.new do |spec| - spec.name = 'not_a_gem' - spec.version = '1.0.0' + context 'with a require in solargraph test bundle' do + let(:requires) do + ['ast'] + end + + it 'generates pins from gems' do + node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } + expect(node_pin).to be_a(Solargraph::Pin::Namespace) end - allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], [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 - workspace = Solargraph::Workspace.new(Dir.pwd) - plain_doc_map = Solargraph::DocMap.new([], [], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], [], workspace) + context 'with an invalid require' do + let(:requires) do + ['not_a_gem'] + end - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + it 'tracks unresolved requires' do + # These are auto-required by solargraph-rspec in case the bundle + # includes these gems. In our case, it doesn't! + unprovided_solargraph_rspec_requires = [ + 'rspec-rails', + 'actionmailer', + 'activerecord', + 'shoulda-matchers', + 'rspec-sidekiq', + 'airborne', + 'activesupport' + ] + expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) + .to eq(['not_a_gem']) + end end 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).with(/path set/) + allow(Solargraph.logger).to receive(:warn).and_call_original Solargraph::DocMap.new(['set'], []) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end + + context 'when deserialization takes a while' do + let(:pre_cache) { false } + let(:requires) { ['backport'] } + + before do + # proxy this method to simulate a long-running deserialization + allow(Benchmark).to receive(:measure) do |&block| + block.call + 5.0 + end + end + + it 'logs timing' do + pending('logging being implemented') + # force lazy evaluation + _pins = doc_map.pins + expect(out.string).to include('Deserialized ').and include(' gem pins ').and include(' ms') + end + end + + 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. + allow(Solargraph.logger).to receive(:warn) + Solargraph::DocMap.new(['set'], [], workspace) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + expect { Solargraph::DocMap.new([nil], [], workspace) }.not_to raise_error end it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + expect { Solargraph::DocMap.new([''], [], workspace) }.not_to raise_error end it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) + doc_map = Solargraph::DocMap.new(['rspec'], [], workspace) expect(doc_map.dependencies.map(&:name)).to include('rspec-core') end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end + context 'with require as bundle/require' do + it 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace) + doc_map_with_bundler_require.cache_all!(nil) + expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive end + end + + context 'with a require not needed by Ruby core' do + let(:requires) { ['set'] } + + it 'does not warn' 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. + allow(Solargraph.logger).to receive(:warn) + doc_map + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end + end + + context 'with a nil require' do + let(:requires) { [nil] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end + end + + context 'with an empty require' do + let(:requires) { [''] } - Solargraph::Convention.register dummy_convention + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end + end + + context 'with a require that has dependencies' do + let(:requires) { ['rspec'] } + + it 'collects dependencies' do + expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + end + end + + context 'with convention' do + let(:pre_cache) { false } - doc_map = Solargraph::DocMap.new(['original_gem'], []) + it 'includes convention requires from environ' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def global(doc_map) + Solargraph::Environ.new( + requires: ['convention_gem1', 'convention_gem2'] + ) + end + end - expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + Solargraph::Convention.register dummy_convention - # Clean up the registered convention - Solargraph::Convention.unregister dummy_convention + doc_map = Solargraph::DocMap.new(['original_gem'], [], workspace) + + expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + ensure + # Clean up the registered convention + Solargraph::Convention.unregister dummy_convention + end end end From 2d0479977a08c6d358c588c559351dc684fbc7a5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 9 Sep 2025 16:46:41 -0400 Subject: [PATCH 159/524] Fix merge errors --- spec/doc_map_spec.rb | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 2624e472d..54b423f99 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -55,14 +55,6 @@ end end - 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. - allow(Solargraph.logger).to receive(:warn).and_call_original - Solargraph::DocMap.new(['set'], []) - expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) - end - context 'when deserialization takes a while' do let(:pre_cache) { false } let(:requires) { ['backport'] } @@ -83,27 +75,6 @@ end end - 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. - allow(Solargraph.logger).to receive(:warn) - Solargraph::DocMap.new(['set'], [], workspace) - expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) - end - - it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], [], workspace) }.not_to raise_error - end - - it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], [], workspace) }.not_to raise_error - end - - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], [], workspace) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') - end - context 'with require as bundle/require' do it 'imports all gems when bundler/require used' do doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace) From be3f5e54853eebbf835964ac139e3ace4ae81318 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 9 Sep 2025 16:55:10 -0400 Subject: [PATCH 160/524] Add fix-ups --- spec/api_map_spec.rb | 2 +- spec/shell_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 01f78846c..805ce49cb 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -766,7 +766,7 @@ def bar; 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) + expect(api_map.super_and_sub?('Boolean', 'false')).to be(true) end it 'resolves aliases for YARD methods' do diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 91f84b4c7..e24220ea9 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -7,12 +7,14 @@ before do File.open(File.join(temp_dir, 'Gemfile'), 'w') do |file| file.puts "source 'https://rubygems.org'" - file.puts "gem 'solargraph', path: #{File.expand_path('..', __dir__)}" + file.puts "gem 'solargraph', path: '#{File.expand_path('..', __dir__)}'" end output, status = Open3.capture2e("bundle install", chdir: temp_dir) raise "Failure installing bundle: #{output}" unless status.success? end + # @type cmd [Array] + # @return [String] def bundle_exec(*cmd) # run the command in the temporary directory with bundle exec output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}", chdir: temp_dir) From 82c94a675f989db005e0e29dba45dafe507c6157 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 12:16:23 -0400 Subject: [PATCH 161/524] Move Pin::Base#infer deprecation to assert system --- lib/solargraph/api_map.rb | 3 ++- lib/solargraph/pin/base.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 02f8f68ea..f41ae0038 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -793,7 +793,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if const.is_a?(Pin::Namespace) result.concat inner_get_methods(const.path, scope, visibility, deep, skip, true) elsif const.is_a?(Pin::Constant) - type = const.infer(self) + type = const.typify(self) + type = const.probe(self) unless type.defined? result.concat inner_get_methods(type.namespace, scope, visibility, deep, skip, true) if type.defined? else referenced_tag = ref.parametrized_tag diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index e6a630562..42b90b77c 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -528,7 +528,7 @@ def probe api_map # @param api_map [ApiMap] # @return [ComplexType] def infer api_map - Solargraph::Logging.logger.warn "WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead." + Solargraph.assert_or_log(:pin_infer, 'WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead.') type = typify(api_map) return type unless type.undefined? probe api_map From 6372fdf19b10d4ffa7408c250517d43e7327b17a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 12:25:04 -0400 Subject: [PATCH 162/524] Let extra line slide pending new constants PR --- .rubocop_todo.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 89fd47c5d..65e9d3feb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.80.2. +# using RuboCop version 1.80.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -433,7 +433,6 @@ Layout/TrailingWhitespace: Exclude: - 'lib/solargraph/language_server/message/client/register_capability.rb' - 'spec/api_map/config_spec.rb' - - 'spec/convention_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -512,11 +511,6 @@ Lint/DuplicateMethods: - 'lib/solargraph/rbs_map/core_map.rb' - 'lib/solargraph/source/chain/link.rb' -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/convention_spec.rb' - # Configuration parameters: AllowComments. Lint/EmptyClass: Exclude: @@ -530,6 +524,7 @@ Lint/EmptyClass: # Configuration parameters: AllowComments. Lint/EmptyFile: Exclude: + - 'lib/solargraph/foo.rb' - 'spec/fixtures/vendored/vendor/do_not_use.gemspec' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -782,6 +777,7 @@ Metrics/ParameterLists: # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: Exclude: + - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source/chain/call.rb' @@ -998,11 +994,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper/to_method_spec.rb' - 'spec/yard_map/mapper_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/convention_spec.rb' - # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterFinalLet: Exclude: @@ -2143,7 +2134,6 @@ Style/RedundantParentheses: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/pin/search.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/type_checker.rb' @@ -2299,7 +2289,6 @@ Style/StringLiterals: - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' - - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/conversions.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' @@ -2614,7 +2603,6 @@ Layout/LineLength: - 'lib/solargraph/workspace.rb' - 'lib/solargraph/workspace/config.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/api_map_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/language_server/message/completion_item/resolve_spec.rb' - 'spec/language_server/message/extended/check_gem_version_spec.rb' From c32c86cc9fbdb45f97aed4c7ead1c20fcfbcaee2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 16:55:27 -0400 Subject: [PATCH 163/524] Point to branch immediately upstream --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index d731fc786..793831b14 100755 --- a/Rakefile +++ b/Rakefile @@ -54,7 +54,7 @@ def undercover cmd = 'bundle exec undercover ' \ '--simplecov coverage/combined/coverage.json ' \ '--exclude-files "Rakefile,spec/*,spec/**/*,lib/solargraph/version.rb" ' \ - '--compare origin/master' + '--compare origin/extract_gemspecs_logic_from_doc_map' output, status = Bundler.with_unbundled_env do Open3.capture2e(cmd) end From bd3ce0b0ed43efb0b1f3401e595a4e2a1dd4d6c7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 18:00:20 -0400 Subject: [PATCH 164/524] Move find_gem implementation, add more specs --- lib/solargraph/workspace.rb | 4 +- lib/solargraph/workspace/gemspecs.rb | 11 +++ spec/workspace/gemspecs_find_gem_spec.rb | 102 +++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 spec/workspace/gemspecs_find_gem_spec.rb diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 1531509af..69ad12bae 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -167,9 +167,7 @@ def rbs_collection_config_path # # @return [Gem::Specification, nil] def find_gem name, version = nil - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError - nil + gemspecs.find_gem(name, version) end # Synchronize the workspace from the provided updater. diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 1f4fef27b..4b4eb0265 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -68,6 +68,17 @@ def resolve_require require [gemspec_or_preference(gemspec)] end + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] output stream for logging + # + # @return [Gem::Specification, nil] + def find_gem name, version = nil, out = nil + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError + nil + end + # @param gemspec [Gem::Specification] # @param out[IO, nil] output stream for logging # diff --git a/spec/workspace/gemspecs_find_gem_spec.rb b/spec/workspace/gemspecs_find_gem_spec.rb new file mode 100644 index 000000000..76caddab3 --- /dev/null +++ b/spec/workspace/gemspecs_find_gem_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'tmpdir' +require 'rubygems/commands/install_command' + +describe Solargraph::Workspace::Gemspecs, '#find_gem' do + subject(:gemspec) { gemspecs.find_gem(name, version, out: out) } + + let(:gemspecs) { described_class.new(dir_path) } + let(:out) { StringIO.new } + + context 'with local bundle' do + let(:dir_path) { File.realpath(Dir.pwd) } + + context 'with solargraph from bundle' do + let(:name) { 'solargraph' } + let(:version) { nil } + + it 'returns the gem' do + expect(gemspec.name).to eq(name) + end + end + + context 'with random from core' do + let(:name) { 'random' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not complain' do + expect(out.string).to be_empty + end + end + + context 'with ripper from core' do + let(:name) { 'ripper' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + end + + context 'with base64 from stdlib' do + let(:name) { 'base64' } + let(:version) { nil } + + it 'returns a gemspec' do + expect(gemspec).not_to be_nil + end + end + + context 'with gem not in bundle' do + let(:name) { 'checkoff' } + let(:version) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + pending("implementation") + gemspec + + expect(out.string).to include('install the gem checkoff ') + end + end + + context 'with gem not in bundle but no logger' do + let(:name) { 'checkoff' } + let(:version) { nil } + let(:out) { nil } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'does not fail' do + expect { gemspec }.not_to raise_error + end + end + + context 'with gem not in bundle with version' do + let(:name) { 'checkoff' } + let(:version) { '1.0.0' } + + it 'returns no gemspec' do + expect(gemspec).to be_nil + end + + it 'complains' do + pending("implementation") + gemspec + + expect(out.string).to include('install the gem checkoff:1.0.0') + end + end + end +end From adb34e55d3ca11e84191acbf4ccea18643da7f12 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 21:02:51 -0400 Subject: [PATCH 165/524] Use find_gem in another location --- lib/solargraph/shell.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 5c3fc0581..4284838d3 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -129,7 +129,7 @@ def uncache *gems next end - spec = Gem::Specification.find_by_name(gem) + spec = api_map.workspace.find_gem(gem) PinCache.uncache_gem(spec, out: $stdout) end end From 97a3ceb841eafb6e2d69c26ec39c822c711abc6d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 22:15:39 -0400 Subject: [PATCH 166/524] Fix workspace calls in shell.rb --- lib/solargraph/shell.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 4284838d3..907daccc6 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -118,6 +118,7 @@ def cache gem, version = nil # @return [void] def uncache *gems raise ArgumentError, 'No gems specified.' if gems.empty? + workspace = Solargraph::Workspace.new(Dir.pwd) gems.each do |gem| if gem == 'core' PinCache.uncache_core @@ -129,7 +130,7 @@ def uncache *gems next end - spec = api_map.workspace.find_gem(gem) + spec = workspace.find_gem(gem) PinCache.uncache_gem(spec, out: $stdout) end end @@ -140,12 +141,13 @@ def uncache *gems # @return [void] def gems *names api_map = ApiMap.load('.') + workspace = api_map.workspace if names.empty? 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 = api_map.workspace.find_gem(*name.split('=')) + spec = workspace.find_gem(*name.split('=')) do_cache spec, api_map rescue Gem::MissingSpecError warn "Gem '#{name}' not found" From 3a7b7e7bc980cfc22371348362d98b0d99e78408 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 10 Sep 2025 22:51:56 -0400 Subject: [PATCH 167/524] Document 'solargraph init' command in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7f344c712..febc972ee 100755 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Plug-ins and extensions are available for the following editors: Solargraph's behavior can be controlled via optional [configuration](https://solargraph.org/guides/configuration) files. The highest priority file is a `.solargraph.yml` file at the root of the project. If not present, any global configuration at `~/.config/solargraph/config.yml` will apply. The path to the global configuration can be overridden with the `SOLARGRAPH_GLOBAL_CONFIG` environment variable. +Use `bundle exec solargraph init` to create a configuration file. + ### Plugins Solargraph supports [plugins](https://solargraph.org/guides/plugins) that implement their own Solargraph features, such as diagnostics reporters and conventions to provide LSP features and type-checking, e.g. for frameworks which use metaprogramming and/or DSLs. From 2ea9620b7e1b96c41d1381d674b0dd4c9d5dc4a5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 08:03:08 -0400 Subject: [PATCH 168/524] Add future spec for issue that blocks existing one --- spec/doc_map_spec.rb | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 54b423f99..51acdbb93 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -33,12 +33,30 @@ end end + context 'understands rspec + rspec-mocks require pattern' do + let(:requires) do + ['rspec-mocks'] + end + + it 'generates pins from gems' do + pending('handling dependencies from conventions as gem names, not requires') + + ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } + expect(ns_pin).to be_a(Solargraph::Pin::Namespace) + end + end + context 'with an invalid require' do let(:requires) do ['not_a_gem'] end - it 'tracks unresolved requires' do + # expected: ["not_a_gem"] + # got: ["not_a_gem", "rspec-mocks"] + # + # This is a gem name vs require name issue coming from conventions + # - will pass once the above context passes + xit 'tracks unresolved requires' do # These are auto-required by solargraph-rspec in case the bundle # includes these gems. In our case, it doesn't! unprovided_solargraph_rspec_requires = [ From 0dbf35846dec2b3a80d31699258598cf253379a9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 08:17:41 -0400 Subject: [PATCH 169/524] Disable new spec --- spec/doc_map_spec.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 51acdbb93..b0efb7d44 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -94,7 +94,19 @@ end context 'with require as bundle/require' do - it 'imports all gems when bundler/require used' do + # @todo need to debug this failure in CI: + # + # Errno::ENOENT: + # No such file or directory - /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/gems/bundler-2.5.22 + # # ./lib/solargraph/yardoc.rb:29:in `cache' + # # ./lib/solargraph/gem_pins.rb:48:in `build_yard_pins' + # # ./lib/solargraph/doc_map.rb:86:in `cache_yard_pins' + # # ./lib/solargraph/doc_map.rb:117:in `cache' + # # ./lib/solargraph/doc_map.rb:75:in `block in cache_all!' + # # ./lib/solargraph/doc_map.rb:74:in `each' + # # ./lib/solargraph/doc_map.rb:74:in `cache_all!' + # # ./spec/doc_map_spec.rb:99:in `block (3 levels) in ' + xit 'imports all gems when bundler/require used' do doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace) doc_map_with_bundler_require.cache_all!(nil) expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive From 4c181eccd77b202a16798a486ec379b517809ef2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 08:26:29 -0400 Subject: [PATCH 170/524] Refactor doc_map_spec --- spec/doc_map_spec.rb | 193 +++++++++++++++++++++++++++++++------------ 1 file changed, 142 insertions(+), 51 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 3f77cd7cc..f557366e0 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -1,82 +1,173 @@ # frozen_string_literal: true +require 'bundler' +require 'benchmark' + describe Solargraph::DocMap do - before :all do - # We use ast here because it's a known dependency. - gemspec = Gem::Specification.find_by_name('ast') - yard_pins = Solargraph::GemPins.build_yard_pins([], gemspec) - Solargraph::PinCache.serialize_yard_gem(gemspec, yard_pins) + subject(:doc_map) do + described_class.new(requires, workspace, out: out) + end + + let(:out) { StringIO.new } + let(:pre_cache) { true } + let(:requires) { [] } + + let(:workspace) do + Solargraph::Workspace.new(Dir.pwd) end - let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } + let(:plain_doc_map) { described_class.new([], workspace, out: nil) } - it 'generates pins from gems' do - doc_map = Solargraph::DocMap.new(['ast'], workspace) - 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) + before do + doc_map.cache_all!(nil) if pre_cache end - it 'tracks unresolved requires' do - doc_map = Solargraph::DocMap.new(['not_a_gem'], workspace) - expect(doc_map.unresolved_requires).to include('not_a_gem') + context 'with a require in solargraph test bundle' do + let(:requires) do + ['ast'] + end + + it 'generates pins from gems' do + node_pin = doc_map.pins.find { |pin| pin.path == 'AST::Node' } + expect(node_pin).to be_a(Solargraph::Pin::Namespace) + end end - it 'tracks uncached_gemspecs' do - gemspec = Gem::Specification.new do |spec| - spec.name = 'not_a_gem' - spec.version = '1.0.0' + context 'understands rspec + rspec-mocks require pattern' do + let(:requires) do + ['rspec-mocks'] + end + + it 'generates pins from gems' do + pending('handling dependencies from conventions as gem names, not requires') + + ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } + expect(ns_pin).to be_a(Solargraph::Pin::Namespace) end - allow(Gem::Specification).to receive(:find_by_path).and_return(gemspec) - doc_map = Solargraph::DocMap.new(['not_a_gem'], workspace) - 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 - plain_doc_map = Solargraph::DocMap.new([], workspace) - doc_map_with_bundler_require = Solargraph::DocMap.new(['bundler/require'], workspace) + context 'with an invalid require' do + let(:requires) do + ['not_a_gem'] + end + + # expected: ["not_a_gem"] + # got: ["not_a_gem", "rspec-mocks"] + # + # This is a gem name vs require name issue coming from conventions + # - will pass once the above context passes + xit 'tracks unresolved requires' do + # These are auto-required by solargraph-rspec in case the bundle + # includes these gems. In our case, it doesn't! + unprovided_solargraph_rspec_requires = [ + 'rspec-rails', + 'actionmailer', + 'activerecord', + 'shoulda-matchers', + 'rspec-sidekiq', + 'airborne', + 'activesupport' + ] + expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) + .to eq(['not_a_gem']) + end + end - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + context 'with an uncached but valid gemspec' do + let(:requires) { ['uncached_gem'] } + let(:pre_cache) { false } + let(:workspace) { instance_double(Solargraph::Workspace) } + + it 'tracks uncached_gemspecs' do + pending('moving cache_stdlib_rbs_map into PinCache') + + pincache = instance_double(Solargraph::PinCache, cache_stdlib_rbs_map: false) + uncached_gemspec = Gem::Specification.new('uncached_gem', '1.0.0') + allow(workspace).to receive_messages(fresh_pincache: pincache, resolve_require: [uncached_gemspec], stdlib_dependencies: [], + fetch_dependencies: []) + allow(Gem::Specification).to receive(:find_by_path).with('uncached_gem').and_return(uncached_gemspec) + allow(workspace).to receive(:global_environ).and_return(Solargraph::Environ.new) + allow(pincache).to receive(:deserialize_combined_pin_cache).with(uncached_gemspec).and_return(nil) + expect(doc_map.uncached_gemspecs).to eq([uncached_gemspec]) + end end - 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. - allow(Solargraph.logger).to receive(:warn) - Solargraph::DocMap.new(['set'], workspace) - expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + context 'with require as bundle/require' do + # @todo need to debug this failure in CI: + # + # Errno::ENOENT: + # No such file or directory - /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/gems/bundler-2.5.22 + # # ./lib/solargraph/yardoc.rb:29:in `cache' + # # ./lib/solargraph/gem_pins.rb:48:in `build_yard_pins' + # # ./lib/solargraph/doc_map.rb:86:in `cache_yard_pins' + # # ./lib/solargraph/doc_map.rb:117:in `cache' + # # ./lib/solargraph/doc_map.rb:75:in `block in cache_all!' + # # ./lib/solargraph/doc_map.rb:74:in `each' + # # ./lib/solargraph/doc_map.rb:74:in `cache_all!' + # # ./spec/doc_map_spec.rb:99:in `block (3 levels) in ' + xit 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace, out: nil) + doc_map_with_bundler_require.cache_doc_map_gems!(nil) + expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive + end end - it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], workspace) }.not_to raise_error + context 'with a require not needed by Ruby core' do + let(:requires) { ['set'] } + + it 'does not warn' 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. + allow(Solargraph.logger).to receive(:warn) + doc_map + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end end - it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], workspace) }.not_to raise_error + context 'with a nil require' do + let(:requires) { [nil] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end end - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], workspace) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + context 'with an empty require' do + let(:requires) { [''] } + + it 'does not raise error' do + expect { doc_map }.not_to raise_error + end end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end + context 'with a require that has dependencies' do + let(:requires) { ['rspec'] } + + it 'collects dependencies' do + expect(doc_map.dependencies.map(&:name)).to include('rspec-core') end + end - Solargraph::Convention.register dummy_convention + context 'with convention' do + let(:pre_cache) { false } - doc_map = Solargraph::DocMap.new(['original_gem'], workspace) + it 'includes convention requires from environ' do + dummy_convention = Class.new(Solargraph::Convention::Base) do + def global(doc_map) + Solargraph::Environ.new( + requires: ['convention_gem1', 'convention_gem2'] + ) + end + end + + Solargraph::Convention.register dummy_convention - expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + doc_map = Solargraph::DocMap.new(['original_gem'], workspace) - # Clean up the registered convention - Solargraph::Convention.unregister dummy_convention + expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') + ensure + # Clean up the registered convention + Solargraph::Convention.unregister dummy_convention + end end end From daf863331cc3959036f607cc774de4ad06a7a002 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 08:41:43 -0400 Subject: [PATCH 171/524] Mark spec with xit --- spec/doc_map_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index b0efb7d44..0d8817116 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -38,7 +38,9 @@ ['rspec-mocks'] end - it 'generates pins from gems' do + # This is a gem name vs require name issue - works under + # solargraph-rspec, but not without + xit 'generates pins from gems' do pending('handling dependencies from conventions as gem names, not requires') ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } From 7762471ebddfdfc1a1a11f9f2f6c91a3e155ebfa Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 08:44:35 -0400 Subject: [PATCH 172/524] Enable xit'ed spec --- spec/doc_map_spec.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index f557366e0..18fef6ff6 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -51,12 +51,7 @@ ['not_a_gem'] end - # expected: ["not_a_gem"] - # got: ["not_a_gem", "rspec-mocks"] - # - # This is a gem name vs require name issue coming from conventions - # - will pass once the above context passes - xit 'tracks unresolved requires' do + it 'tracks unresolved requires' do # These are auto-required by solargraph-rspec in case the bundle # includes these gems. In our case, it doesn't! unprovided_solargraph_rspec_requires = [ From 7a88bd05cc8697442136f10c08816e756f086b39 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 09:23:52 -0400 Subject: [PATCH 173/524] Add assert --- lib/solargraph/workspace/gemspecs.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 4b4eb0265..a796cbabf 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -57,6 +57,7 @@ def resolve_require require file = "lib/#{require}.rb" # @sg-ignore Unresolved call to files gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } + Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") if gemspec.nil? rescue Gem::MissingSpecError logger.debug do "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" From 436f9708a1e84af78da63c5872944a74ded2b77a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 09:26:15 -0400 Subject: [PATCH 174/524] Add failing spec --- spec/workspace/gemspecs_resolve_require_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index 2807c3384..b3fefa840 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -214,6 +214,18 @@ def configure_bundler_spec stub_value end end + context 'with Gemfile and deep require into a gem' do + before { add_bundle } + + let(:require) { 'bundler/gem_tasks' } + + it 'returns gems' do + pending('improved logic for require lookups') + + expect(specs&.map(&:name)).to include('bundler') + end + end + context 'with Gemfile but an unknown gem' do before { add_bundle } From 70c60a4e36b8f459341d2e999a5294bbc2303acf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 10:11:47 -0400 Subject: [PATCH 175/524] Use xit to exclude spec that fails in some environment combinations --- spec/workspace/gemspecs_resolve_require_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index b3fefa840..eb2ab1f58 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -219,7 +219,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/gem_tasks' } - it 'returns gems' do + xit 'returns gems' do pending('improved logic for require lookups') expect(specs&.map(&:name)).to include('bundler') From 602d803000c3d598fa005cda362c78d6efc562e0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 10:20:20 -0400 Subject: [PATCH 176/524] Add another regression spec --- spec/workspace/gemspecs_resolve_require_spec.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index eb2ab1f58..4ffcd0bd1 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -214,7 +214,7 @@ def configure_bundler_spec stub_value end end - context 'with Gemfile and deep require into a gem' do + context 'with Gemfile and deep require into a possibly-core gem' do before { add_bundle } let(:require) { 'bundler/gem_tasks' } @@ -226,6 +226,16 @@ def configure_bundler_spec stub_value end end + context 'with Gemfile and deep require into a gem' do + before { add_bundle } + + let(:require) { 'rspec/mocks' } + + it 'returns gems' do + expect(specs&.map(&:name)).to include('rspec-mocks') + end + end + context 'with Gemfile but an unknown gem' do before { add_bundle } From 569bf4f3d97120c8264657a9322b5ad2f83140d1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 10:41:48 -0400 Subject: [PATCH 177/524] Improve assertion --- lib/solargraph/pin_cache.rb | 17 +++++++++++++++++ lib/solargraph/workspace/gemspecs.rb | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 2a0ec4639..c72a6a770 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -7,6 +7,23 @@ module PinCache class << self include Logging + # @return [Array] a list of possible standard library names + def possible_stdlibs + @possible_stdlibs ||= begin + # all dirs and .rb files in Gem::RUBYGEMS_DIR + Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| + basename = File.basename(file_or_dir) + # remove .rb + basename = basename[0..-4] if basename.end_with?('.rb') + basename + end.sort.uniq + rescue StandardError => e + logger.info { "Failed to get possible stdlibs: #{e.message}" } + logger.debug { e.backtrace.join("\n") } + [] + end + end + # The base directory where cached YARD documentation and serialized pins are serialized # # @return [String] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index a796cbabf..40826879a 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -57,7 +57,9 @@ def resolve_require require file = "lib/#{require}.rb" # @sg-ignore Unresolved call to files gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } - Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") if gemspec.nil? + if gemspec.nil? && !PinCache.possible_stdlibs.include?(gem_name_guess) + Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") + end rescue Gem::MissingSpecError logger.debug do "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" From 396c11406b24dd5ce5fc163a333bddae8c6b20d7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 11:54:05 -0400 Subject: [PATCH 178/524] Drop assert, add gem lookup workaround for conventions, enable spec --- lib/solargraph/workspace/gemspecs.rb | 18 +++++++++++---- spec/doc_map_spec.rb | 34 +++++----------------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 40826879a..31c5908c3 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -57,17 +57,23 @@ def resolve_require require file = "lib/#{require}.rb" # @sg-ignore Unresolved call to files gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } - if gemspec.nil? && !PinCache.possible_stdlibs.include?(gem_name_guess) - Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") - end rescue Gem::MissingSpecError logger.debug do "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" end - [] end end - return nil if gemspec.nil? + # @todo the 'requires' provided in Environ is being used + # by plugins to pass gem names instead of require paths + # - need to expand Environ to provide a place to put gem + # names and get new plugins out before retiring this. + gemspec ||= find_gem(gem_name_guess) + if gemspec.nil? + if !PinCache.possible_stdlibs.include?(gem_name_guess) && !['rspec-rails', 'actionmailer', 'activesupport', 'activerecord', 'shoulda-matchers', 'rspec-sidekiq', 'airborne'].include?(gem_name_guess) + Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") + end + return nil + end [gemspec_or_preference(gemspec)] end @@ -156,6 +162,7 @@ def auto_required_gemspecs_from_bundler logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with " \ 'find_by_name, falling back to guess') # can happen in local filesystem references + # TODO: should this be resolve_require or find_gem? specs = resolve_require lazy_spec.name logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? next specs @@ -190,6 +197,7 @@ def gemspecs_required_from_external_bundle rescue Gem::MissingSpecError => e logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") # can happen in local filesystem references + # TODO: should this be resolve_require or find_gem? specs = resolve_require name logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? next specs diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 18fef6ff6..fad6d3363 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -33,19 +33,6 @@ end end - context 'understands rspec + rspec-mocks require pattern' do - let(:requires) do - ['rspec-mocks'] - end - - it 'generates pins from gems' do - pending('handling dependencies from conventions as gem names, not requires') - - ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } - expect(ns_pin).to be_a(Solargraph::Pin::Namespace) - end - end - context 'with an invalid require' do let(:requires) do ['not_a_gem'] @@ -88,21 +75,9 @@ end context 'with require as bundle/require' do - # @todo need to debug this failure in CI: - # - # Errno::ENOENT: - # No such file or directory - /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/gems/bundler-2.5.22 - # # ./lib/solargraph/yardoc.rb:29:in `cache' - # # ./lib/solargraph/gem_pins.rb:48:in `build_yard_pins' - # # ./lib/solargraph/doc_map.rb:86:in `cache_yard_pins' - # # ./lib/solargraph/doc_map.rb:117:in `cache' - # # ./lib/solargraph/doc_map.rb:75:in `block in cache_all!' - # # ./lib/solargraph/doc_map.rb:74:in `each' - # # ./lib/solargraph/doc_map.rb:74:in `cache_all!' - # # ./spec/doc_map_spec.rb:99:in `block (3 levels) in ' - xit 'imports all gems when bundler/require used' do - doc_map_with_bundler_require = described_class.new(['bundler/require'], [], workspace, out: nil) - doc_map_with_bundler_require.cache_doc_map_gems!(nil) + it 'imports all gems when bundler/require used' do + doc_map_with_bundler_require = described_class.new(['bundler/require'], workspace, out: nil) + doc_map_with_bundler_require.cache_all!(nil) expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive end end @@ -159,6 +134,9 @@ def global(doc_map) doc_map = Solargraph::DocMap.new(['original_gem'], workspace) + # @todo this should probably not be in requires, which is a + # path, and instead be in a new gem_names property on the + # Environ expect(doc_map.requires).to include('original_gem', 'convention_gem1', 'convention_gem2') ensure # Clean up the registered convention From 6e03bc8ae2e4d973397db0e77c9764e3bcbe9f25 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 14:19:00 -0400 Subject: [PATCH 179/524] Handle bad gemdir from gemspec object --- lib/solargraph/gem_pins.rb | 1 + lib/solargraph/yardoc.rb | 9 +++++++++ spec/gem_pins_spec.rb | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index a193a8a39..ba362c351 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -46,6 +46,7 @@ def self.combine_method_pins(*pins) # @return [Array] def self.build_yard_pins(yard_plugins, gemspec) Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec) + return [] unless Yardoc.cached?(gemspec) yardoc = Yardoc.load!(gemspec) YardMap::Mapper.new(yardoc, gemspec).map end diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 625e41ce4..43424d99a 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -18,6 +18,15 @@ def cache(yard_plugins, gemspec) path = PinCache.yardoc_path gemspec return path if cached?(gemspec) + unless Dir.exist? gemspec.gem_dir + # Can happen in at least some (old?) RubyGems versions when we + # have a gemspec describing a standard library like bundler. + # + # https://github.com/apiology/solargraph/actions/runs/17650140201/job/50158676842?pr=10 + Solargraph.logger.info { "Bad info from gemspec - #{gemspec.gem_dir} does not exist" } + return path + end + Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}" cmd = "yardoc --db #{path} --no-output --plugin solargraph" yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index d630784cf..c3c18109d 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -11,4 +11,9 @@ expect(core_root.return_type.to_s).to eq('Pathname, nil') expect(core_root.location.filename).to end_with('environment_loader.rb') end + + it "does not error out when handed incorrect gemspec" do + gemspec = instance_double(Gem::Specification, name: 'foo', version: '1.0', gem_dir: "/not-there") + expect { Solargraph::GemPins.build_yard_pins([], gemspec) }.not_to raise_error + end end From 62d7d0833bd366a7b98690e157a1dd79be2db658 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 14:29:03 -0400 Subject: [PATCH 180/524] Linting fix --- spec/gem_pins_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index c3c18109d..8e3962341 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -12,8 +12,8 @@ expect(core_root.location.filename).to end_with('environment_loader.rb') end - it "does not error out when handed incorrect gemspec" do - gemspec = instance_double(Gem::Specification, name: 'foo', version: '1.0', gem_dir: "/not-there") + it 'does not error out when handed incorrect gemspec' do + gemspec = instance_double(Gem::Specification, name: 'foo', version: '1.0', gem_dir: '/not-there') expect { Solargraph::GemPins.build_yard_pins([], gemspec) }.not_to raise_error end end From 9b58947b4beada2bc5cf4faff9915a59bdcbedf8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 15:24:35 -0400 Subject: [PATCH 181/524] xit -> pending so we reenable specs once fixed --- spec/api_map_spec.rb | 5 ++-- spec/complex_type_spec.rb | 15 ++++++++---- spec/parser/node_chainer_spec.rb | 4 +++- spec/parser/node_methods_spec.rb | 4 +++- spec/rbs_map/core_map_spec.rb | 4 +++- spec/source/chain/call_spec.rb | 12 +++++++--- spec/source/chain_spec.rb | 4 +++- spec/source_map/clip_spec.rb | 32 ++++++++++++++++++------- spec/type_checker/levels/strict_spec.rb | 16 +++++++++---- spec/yard_map/mapper/to_method_spec.rb | 4 +++- 10 files changed, 74 insertions(+), 26 deletions(-) diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index 805ce49cb..bb76679f1 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -430,8 +430,9 @@ class Sup expect(pins.map(&:path)).to include('Mixin#bar') end - # pending https://github.com/apiology/solargraph/pull/4 - xit 'understands tuples inherit from regular arrays' do + it 'understands tuples inherit from regular arrays' do + pending('Fix to remove trailing generic<> after resolution') + method_pins = @api_map.get_method_stack("Array(1, 2, 'a')", 'include?') method_pin = method_pins.first expect(method_pin).to_not be_nil diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index f876d642f..b97338a6c 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -13,7 +13,8 @@ expect(types.length).to eq(0) end - xit 'parses zero types as a string' do + it 'parses zero types as a string' do + pending('special case being added') types = Solargraph::ComplexType.parse '' expect(types.length).to eq(0) end @@ -265,14 +266,18 @@ # 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 + it 'understands literal strings with double quotes' do + pending('string escaping support being added') + 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 + it 'understands literal strings with single quotes' do + pending('string escaping support being added') + type = Solargraph::ComplexType.parse("'foo'") expect(type.tag).to eq("'foo'") expect(type.to_rbs).to eq("'foo'") @@ -725,7 +730,9 @@ def make_bar expect(result.to_rbs).to eq('::Array[::String]') end - xit 'stops parsing when the first character indicates a string literal' do + it 'stops parsing when the first character indicates a string literal' do + pending('string escaping support being added') + api_map = Solargraph::ApiMap.new type = Solargraph::ComplexType.parse('"Array(Symbol, String, Array(Integer, Integer)"') type = type.qualify(api_map) diff --git a/spec/parser/node_chainer_spec.rb b/spec/parser/node_chainer_spec.rb index e92431aae..85fa140d8 100644 --- a/spec/parser/node_chainer_spec.rb +++ b/spec/parser/node_chainer_spec.rb @@ -141,7 +141,9 @@ class Foo expect(chain.links.first).to be_with_block end - xit 'tracks complex multiple assignment' do + it 'tracks complex multiple assignment' do + pending('complex multiple assignment support') + source = Solargraph::Source.load_string(%( foo.baz, bar = [1, 2] )) diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index f9504b584..536bc61d6 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -289,7 +289,9 @@ expect(rets.length).to eq(1) end - xit "short-circuits return node finding after a raise statement in a begin expressiona" do + it "short-circuits return node finding after a raise statement in a begin expression" do + pending('case being handled') + node = Solargraph::Parser.parse(%( raise "Error" y diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 88590925b..3f9cc6e03 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -58,7 +58,9 @@ expect(signature.block.parameters.map(&:return_type).map(&:to_s)).to eq(['String']) end - xit 'understands defaulted type parameters' do + it 'understands defaulted type parameters' do + pending('defaulted type parameter support') + # @todo Enumerable#each's' return type not yet supported as _Each<> # takes two type parameters, the second has a default value, # Enumerable specifies it, but Solargraph doesn't support type diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 8b67a3c66..e27203fd4 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -250,7 +250,9 @@ def baz expect(type.simple_tags).to eq('Integer') end - xit 'infers method return types based on method generic' do + it 'infers method return types based on method generic' do + pending('deeper inference support') + source = Solargraph::Source.load_string(%( class Foo # @Generic A @@ -315,7 +317,9 @@ def baz expect(type.tag).to eq('String') end - xit 'infers generic return types from block from yield being a return node' do + it 'infers generic return types from block from yield being a return node' do + pending('deeper inference support') + source = Solargraph::Source.load_string(%( def yielder(&blk) yield @@ -595,7 +599,9 @@ def k expect(clip.infer.rooted_tags).to eq('::Array<::A::D::E>') end - xit 'correctly looks up civars' do + it 'correctly looks up civars' do + pending('better civar support') + source = Solargraph::Source.load_string(%( class Foo BAZ = /aaa/ diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index abc8c2b05..ec96e800f 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -362,7 +362,9 @@ class Bar; end expect(chain.links[1]).to be_with_block end - xit 'infers instance variables from multiple assignments' do + it 'infers instance variables from sequential assignments' do + pending('sequential assignment support') + source = Solargraph::Source.load_string(%( def foo @foo = nil diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 0f83331ec..fe6a14723 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -320,7 +320,9 @@ def foo expect(type.simple_tags).to eq('String, Integer') end - xit 'uses flow-sensitive typing to infer non-nil method return type' do + it 'uses flow-sensitive typing to infer non-nil method return type' do + pending('if x.nil? support in flow sensitive typing') + source = Solargraph::Source.load_string(%( # @return [Gem::Specification,nil] def find_by_name; end @@ -2626,7 +2628,9 @@ def bar; end expect(clip.infer.to_s).to eq('Foo') end - xit 'replaces nil with reassignments' do + it 'replaces nil with reassignments' do + pending 'sequential assignment support' + source = Solargraph::Source.load_string(%( bar = nil bar @@ -2641,7 +2645,9 @@ def bar; end expect(clip.infer.to_s).to eq('Integer') end - xit 'replaces type with reassignments' do + it 'replaces type with reassignments' do + pending 'sequential assignment support' + source = Solargraph::Source.load_string(%( bar = 'a' bar @@ -2669,7 +2675,9 @@ def bar; end expect(clip.infer.to_s).to eq('String, nil') end - xit 'replaces nil with alternate reassignments' do + it 'replaces nil with alternate reassignments' do + pending 'conditional assignment support' + source = Solargraph::Source.load_string(%( bar = nil if baz @@ -2684,7 +2692,9 @@ def bar; end expect(clip.infer.to_s).to eq('Symbol, Integer') end - xit 'replaces type with alternate reassignments' do + it 'replaces type with alternate reassignments' do + pending 'conditional assignment support' + source = Solargraph::Source.load_string(%( bar = 'a' if baz @@ -2949,7 +2959,9 @@ def foo expect(clip.infer.to_s).to eq('Array, Hash, Integer, nil') end - xit 'infers that type of argument has been overridden' do + it 'infers that type of argument has been overridden' do + pending 'sequential assignment support' + source = Solargraph::Source.load_string(%( def foo a a = 'foo' @@ -2962,7 +2974,9 @@ def foo a expect(clip.infer.to_s).to eq('String') end - xit 'preserves hash value when it is a union with brackets' do + it 'preserves hash value when it is a union with brackets' do + pending 'union in bracket support' + source = Solargraph::Source.load_string(%( # @type [Hash{String => [Array, Hash, Integer, nil]}] raw_data = {} @@ -2988,7 +3002,9 @@ def foo a expect(clip.infer.to_s).to eq('Array') end - xit 'preserves hash value when it is a union with brackets' do + it 'preserves hash value when it is a union with brackets' do + pending 'union in bracket support' + source = Solargraph::Source.load_string(%( # @type [Hash{String => [Array, Hash, Integer, nil]}] raw_data = {} diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 7e57cb7cf..a408544f5 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -539,7 +539,9 @@ def bar(baz:, bing:) expect(checker.problems).to be_empty end - xit 'requires strict return tags' do + it 'requires strict return tags' do + pending 'nil? support in flow sensitive typing' + checker = type_checker(%( class Foo # The tag is [String] but the inference is [String, nil] @@ -554,7 +556,9 @@ def bar expect(checker.problems.first.message).to include('does not match inferred type') end - xit 'requires strict return tags' do + it 'requires strict return tags' do + pending 'nil? support in flow sensitive typing' + checker = type_checker(%( class Foo # The tag is [String] but the inference is [String, nil] @@ -755,7 +759,9 @@ def meth(param1) expect(checker.problems).to be_one end - xit 'uses nil? to refine type' do + it 'uses nil? to refine type' do + pending 'nil? support in flow sensitive typing' + checker = type_checker(%( # @sg-ignore # @type [String, nil] @@ -859,7 +865,9 @@ def foo *path, baz; end expect(checker.problems.map(&:message)).to be_empty end - xit "Uses flow scope to specialize understanding of cvar types" do + it "Uses flow scope to specialize understanding of cvar types" do + pending 'better cvar support' + checker = type_checker(%( class Bar # @return [String] diff --git a/spec/yard_map/mapper/to_method_spec.rb b/spec/yard_map/mapper/to_method_spec.rb index 9c5caa705..c90fe75ed 100644 --- a/spec/yard_map/mapper/to_method_spec.rb +++ b/spec/yard_map/mapper/to_method_spec.rb @@ -67,7 +67,9 @@ expect(param.full).to eq("&bar") end - xit 'parses undefined but typed blockargs' do + it 'parses undeclared but typed blockargs' do + pending('block args coming from YARD alone') + code_object.parameters = [] code_object.docstring = < Date: Thu, 11 Sep 2025 14:36:53 -0400 Subject: [PATCH 182/524] Handle a solargraph-rails case --- spec/source_map/clip_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 0f83331ec..3fea653f7 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1647,7 +1647,9 @@ def foo; end expect(array_names).to eq(["byteindex", "byterindex", "bytes", "bytesize", "byteslice", "bytesplice"]) string_names = api_map.clip_at('test.rb', [6, 22]).complete.pins.map(&:name) - expect(string_names).to eq(['upcase', 'upcase!', 'upto']) + # can be brought in by solargraph-rails + activesupport_completions = ['upcase_first'] + expect(string_names - activesupport_completions).to eq(['upcase', 'upcase!', 'upto']) end it 'completes global methods defined in top level scope inside class when referenced inside a namespace' do From d85fc6526448a8f6580b72a9b47d260c5a4bb3e7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 15:54:04 -0400 Subject: [PATCH 183/524] Reenable specs --- spec/workspace/gemspecs_resolve_require_spec.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index 4ffcd0bd1..6ee0cecf0 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -180,9 +180,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/require' } it 'finds nothing' do - pending('https://github.com/castwide/solargraph/pull/1006') - - expect(specs).to be_empty + expect(specs).to be_nil end end end @@ -219,9 +217,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/gem_tasks' } - xit 'returns gems' do - pending('improved logic for require lookups') - + it 'returns gems' do expect(specs&.map(&:name)).to include('bundler') end end From 73f98b9ee3ab7914c899837d54ca418b2b6fffce Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 16:08:14 -0400 Subject: [PATCH 184/524] Add a regression spec --- spec/workspace/gemspecs_fetch_dependencies_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb index 285f8e1a0..6ead1060a 100644 --- a/spec/workspace/gemspecs_fetch_dependencies_spec.rb +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -22,6 +22,16 @@ end end + context 'with a Gem::Specification' do + let(:gemspec) do + Gem::Specification.find_by_name('solargraph') + end + + it 'finds a known dependency' do + expect(deps.map(&:name)).to include('backport') + end + end + context 'with gem whose dependency does not exist in our bundle' do let(:gemspec) do instance_double(Gem::Specification, From cb59ba6bebbc35461a47bc97642a1d5cc4d0cb4f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 16:11:40 -0400 Subject: [PATCH 185/524] Drop assert --- lib/solargraph/pin_cache.rb | 17 ----------------- lib/solargraph/workspace/gemspecs.rb | 8 ++------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index c72a6a770..2a0ec4639 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -7,23 +7,6 @@ module PinCache class << self include Logging - # @return [Array] a list of possible standard library names - def possible_stdlibs - @possible_stdlibs ||= begin - # all dirs and .rb files in Gem::RUBYGEMS_DIR - Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| - basename = File.basename(file_or_dir) - # remove .rb - basename = basename[0..-4] if basename.end_with?('.rb') - basename - end.sort.uniq - rescue StandardError => e - logger.info { "Failed to get possible stdlibs: #{e.message}" } - logger.debug { e.backtrace.join("\n") } - [] - end - end - # The base directory where cached YARD documentation and serialized pins are serialized # # @return [String] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 31c5908c3..05acb56c9 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -68,12 +68,8 @@ def resolve_require require # - need to expand Environ to provide a place to put gem # names and get new plugins out before retiring this. gemspec ||= find_gem(gem_name_guess) - if gemspec.nil? - if !PinCache.possible_stdlibs.include?(gem_name_guess) && !['rspec-rails', 'actionmailer', 'activesupport', 'activerecord', 'shoulda-matchers', 'rspec-sidekiq', 'airborne'].include?(gem_name_guess) - Solargraph.assert_or_log(:gemspecs_resolve_require_guess, "Could not find require based on #{require.inspect}") - end - return nil - end + return nil if gemspec.nil? + [gemspec_or_preference(gemspec)] end From 05b36ec01808b6af7d898f49a976eb7ec820ca30 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 11 Sep 2025 16:25:18 -0400 Subject: [PATCH 186/524] Import spec to cover shell.rb changes --- lib/solargraph/shell.rb | 2 +- spec/shell_spec.rb | 159 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 153 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 907daccc6..117bbbe2d 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -267,7 +267,7 @@ def pin_description pin 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) + api_map.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) end end end diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 91f84b4c7..f56cebc7d 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -1,18 +1,23 @@ +# frozen_string_literal: true + require 'tmpdir' require 'open3' describe Solargraph::Shell do + let(:shell) { described_class.new } let(:temp_dir) { Dir.mktmpdir } before do File.open(File.join(temp_dir, 'Gemfile'), 'w') do |file| file.puts "source 'https://rubygems.org'" - file.puts "gem 'solargraph', path: #{File.expand_path('..', __dir__)}" + file.puts "gem 'solargraph', path: '#{File.expand_path('..', __dir__)}'" end output, status = Open3.capture2e("bundle install", chdir: temp_dir) raise "Failure installing bundle: #{output}" unless status.success? end + # @type cmd [Array] + # @return [String] def bundle_exec(*cmd) # run the command in the temporary directory with bundle exec output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}", chdir: temp_dir) @@ -25,20 +30,160 @@ def bundle_exec(*cmd) FileUtils.rm_rf(temp_dir) end - describe "--version" do - it "returns a version when run" do - output = bundle_exec("solargraph", "--version") + describe '--version' do + let(:output) { bundle_exec('solargraph', '--version') } + it 'returns output' do expect(output).not_to be_empty + end + + it 'returns a version when run' do expect(output).to eq("#{Solargraph::VERSION}\n") end end - describe "uncache" do - it "uncaches without erroring out" do - output = bundle_exec("solargraph", "uncache", "solargraph") + describe 'uncache' do + it 'uncaches without erroring out' do + output = capture_stdout do + shell.uncache('backport') + end expect(output).to include('Clearing pin cache in') end + + it 'uncaches stdlib without erroring out' do + expect { shell.uncache('stdlib') }.not_to raise_error + end + + it 'uncaches core without erroring out' do + expect { shell.uncache('core') }.not_to raise_error + end + end + + describe 'scan' do + context 'with mocked dependencies' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + end + + it 'scans without erroring out' do + allow(api_map).to receive(:pins).and_return([]) + output = capture_stdout do + shell.options = { directory: 'spec/fixtures/workspace' } + shell.scan + end + + expect(output).to include('Scanned ').and include(' seconds.') + end + end + end + + describe 'typecheck' do + context 'with mocked dependencies' do + let(:type_checker) { instance_double(Solargraph::TypeChecker) } + let(:api_map) { instance_double(Solargraph::ApiMap) } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(Solargraph::TypeChecker).to receive(:new).and_return(type_checker) + allow(type_checker).to receive(:problems).and_return([]) + end + + it 'typechecks without erroring out' do + output = capture_stdout do + shell.options = { level: 'normal', directory: '.' } + shell.typecheck('Gemfile') + end + + expect(output).to include('Typecheck finished in') + end + end + end + + describe 'gems' do + context 'without mocked ApiMap' do + it 'complains when gem does not exist' do + pending 'error message improvements' + + output = capture_both do + shell.gems('nonexistentgem') + end + + expect(output).to include("Gem 'nonexistentgem' not found") + end + + it 'caches core without erroring out' do + pending 'core caching suppport' + + capture_both do + shell.uncache('core') + end + + expect { shell.cache('core') }.not_to raise_error + end + + it 'gives sensible error for gem that does not exist' do + pending 'error message improvements' + + output = capture_both do + shell.gems('solargraph123') + end + + expect(output).to include("Gem 'solargraph123' not found") + end + end + + context 'with mocked Workspace' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:workspace) { instance_double(Solargraph::Workspace) } + let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } + + before do + allow(Solargraph::Workspace).to receive(:new).and_return(workspace) + allow(Solargraph::ApiMap).to receive(:load).with('.').and_return(api_map) + allow(api_map).to receive(:cache_gem) + allow(api_map).to receive(:workspace).and_return(workspace) + end + + it 'caches all without erroring out' do + pending 'delegation to api_map' + + allow(api_map).to receive(:cache_all!) + + _output = capture_both { shell.gems } + + expect(api_map).to have_received(:cache_all!) + end + + it 'caches single gem without erroring out' do + allow(workspace).to receive(:find_gem).with('backport').and_return(gemspec) + + capture_both do + shell.options = { rebuild: false } + shell.gems('backport') + end + + expect(api_map).to have_received(:cache_gem).with(gemspec, out: an_instance_of(StringIO), rebuild: false) + end + end + end + + describe 'cache' do + it 'caches a stdlib gem without erroring out' do + expect { shell.cache('stringio') }.not_to raise_error + end + + context 'when gem does not exist' do + subject(:call) { shell.cache('nonexistentgem8675309') } + + it 'gives a good error message' do + pending 'better error message' + + # capture stderr output + expect { call }.to output(/not found/).to_stderr + end + end end end From db725f78fdb3756c917044e35dd6d1eec1c86095 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 13 Sep 2025 16:30:39 -0400 Subject: [PATCH 187/524] Update rubocop todo --- .rubocop_todo.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 89f703d23..f7e83b95f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -285,7 +285,6 @@ Layout/TrailingWhitespace: # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' # This cop supports safe autocorrection (--autocorrect). @@ -1108,7 +1107,6 @@ Style/RedundantFreeze: # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: - - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -1133,7 +1131,6 @@ Style/RedundantRegexpEscape: # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' @@ -1268,7 +1265,11 @@ Style/YAMLFileRead: # This cop supports unsafe autocorrection (--autocorrect-all). Style/ZeroLengthPredicate: - Enabled: false + Exclude: + - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/pin/method.rb' + - 'lib/solargraph/source/chain/array.rb' + - 'spec/language_server/protocol_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. From 805276e023d95d8601f9f93b333935c0c2623b80 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 13 Sep 2025 16:34:41 -0400 Subject: [PATCH 188/524] Fix typechecking issues --- lib/solargraph/api_map/store.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index b82e5612c..d89d1a489 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -101,7 +101,7 @@ def qualify_superclass fq_sub_tag end # @param fqns [String] - # @return [Array] + # @return [Array] def get_includes fqns include_references[fqns] || [] end @@ -113,7 +113,7 @@ def get_prepends fqns end # @param fqns [String] - # @return [Array] + # @return [Array] def get_extends fqns extend_references[fqns] || [] end From b6d86ecb49631fc9201aa02e9e0626be85147c23 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 13 Sep 2025 17:37:08 -0400 Subject: [PATCH 189/524] Fix merge failure --- lib/solargraph/api_map.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 194e6cd3a..c27fceb7b 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -705,21 +705,6 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop methods end - # @param fq_sub_tag [String] - # @return [String, nil] - def qualify_superclass fq_sub_tag - fq_sub_type = ComplexType.try_parse(fq_sub_tag) - fq_sub_ns = fq_sub_type.name - sup_tag = store.get_superclass(fq_sub_tag) - sup_type = ComplexType.try_parse(sup_tag) - sup_ns = sup_type.name - return nil if sup_tag.nil? - parts = fq_sub_ns.split('::') - last = parts.pop - parts.pop if last == sup_ns - qualify(sup_tag, parts.join('::')) - end - private # A hash of source maps with filename keys. From ca4f03922843102de8ac2ada050a264e095bab9f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 13 Sep 2025 19:35:01 -0400 Subject: [PATCH 190/524] Rebuild rubocop todo --- .rubocop_todo.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f7e83b95f..ecc699d97 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -584,7 +584,6 @@ RSpec/BeforeAfterAll: - '**/spec/rails_helper.rb' - '**/spec/support/**/*.rb' - 'spec/api_map_spec.rb' - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' @@ -675,7 +674,6 @@ RSpec/LetBeforeExamples: # SupportedStyles: have_received, receive RSpec/MessageSpies: Exclude: - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/diagnoser_spec.rb' - 'spec/language_server/host/message_worker_spec.rb' @@ -704,7 +702,11 @@ RSpec/NotToNot: - 'spec/rbs_map/core_map_spec.rb' RSpec/PendingWithoutReason: - Enabled: false + Exclude: + - 'spec/api_map_spec.rb' + - 'spec/doc_map_spec.rb' + - 'spec/pin/local_variable_spec.rb' + - 'spec/type_checker/levels/strict_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. From df2573539463c71815ed97ee4ed1d3347a03cf64 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 14 Sep 2025 20:45:04 -0400 Subject: [PATCH 191/524] Fix types --- lib/solargraph/api_map.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index eb86aa4b1..5ea0166f0 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -306,13 +306,13 @@ def dereference(pin) end # @param fqns [String] - # @return [Array] + # @return [Array] def get_extends(fqns) store.get_extends(fqns) end # @param fqns [String] - # @return [Array] + # @return [Array] def get_includes(fqns) store.get_includes(fqns) end @@ -762,6 +762,9 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| + # @sg-ignore Declared type Solargraph::Pin::Constant does + # not match inferred type Solargraph::Pin::Constant, + # Solargraph::Pin::Namespace, nil for variable const const = get_constants('', *ref.closure.gates).find { |pin| pin.path.end_with? ref.name } if const.is_a?(Pin::Namespace) result.concat inner_get_methods(const.path, scope, visibility, deep, skip, true) From f74f255442ede1a9ae76dcc80d77012367186b68 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 14 Sep 2025 20:45:23 -0400 Subject: [PATCH 192/524] Fix types --- lib/solargraph/api_map/index.rb | 8 ++++---- lib/solargraph/api_map/store.rb | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index b06366f97..c311a9782 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -43,7 +43,7 @@ def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references @include_references ||= Hash.new { |h, k| h[k] = [] } end @@ -53,17 +53,17 @@ def include_reference_pins @include_reference_pins ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references @extend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references @prepend_references ||= Hash.new { |h, k| h[k] = [] } end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references @superclass_references ||= Hash.new { |h, k| h[k] = [] } end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index d89d1a489..545d99540 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -79,7 +79,7 @@ def get_methods fqns, scope: :instance, visibility: [:public] OBJECT_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Object', closure: Pin::ROOT_PIN, source: :solargraph) # @param fqns [String] - # @return [Pin::Reference::Superclass] + # @return [Pin::Reference::Superclass, nil] def get_superclass fqns return nil if fqns.nil? || fqns.empty? return BOOLEAN_SUPERCLASS_PIN if %w[TrueClass FalseClass].include?(fqns) @@ -298,12 +298,12 @@ def symbols index.pins_by_class(Pin::Symbol) end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def superclass_references index.superclass_references end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def include_references index.include_references end @@ -313,12 +313,12 @@ def include_reference_pins index.include_reference_pins end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def prepend_references index.prepend_references end - # @return [Hash{String => Array}] + # @return [Hash{String => Array}] def extend_references index.extend_references end From 135e50db2a27741618e9d76c39b4ec565d6df800 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 14 Sep 2025 21:18:22 -0400 Subject: [PATCH 193/524] Fix types --- lib/solargraph/api_map/store.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 545d99540..a286b851b 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -101,7 +101,7 @@ def qualify_superclass fq_sub_tag end # @param fqns [String] - # @return [Array] + # @return [Array] def get_includes fqns include_references[fqns] || [] end @@ -113,7 +113,7 @@ def get_prepends fqns end # @param fqns [String] - # @return [Array] + # @return [Array] def get_extends fqns extend_references[fqns] || [] end From 67da9b12d2ebfdca0af49b2c5ab77ae9df8a4bf6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 17 Sep 2025 15:18:22 -0400 Subject: [PATCH 194/524] Fix method intersection logic --- lib/solargraph/source/chain/call.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index c9d2c96e5..d2e4b8757 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -56,21 +56,19 @@ def resolve api_map, name_pin, locals [] end return inferred_pins(found, api_map, name_pin, locals) unless found.empty? - if api_map.loose_unions - # fetch methods which ANY of the potential context types provide - pins = name_pin.binder.each_unique_type.flat_map do |context| - ns_tag = context.namespace == '' ? '' : context.namespace_type.tag - stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) - [stack.first].compact - end - else - # grab pins which are provided by every potential context type - pins = name_pin.binder.each_unique_type.map do |context| - ns_tag = context.namespace == '' ? '' : context.namespace_type.tag - stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) - [stack.first].compact - end.reduce(:&) + # @sg-ignore Unresolved call to map on void, ::Enumerator<::Solargraph::ComplexType::UniqueType> + pin_groups = name_pin.binder.each_unique_type.map do |context| + ns_tag = context.namespace == '' ? '' : context.namespace_type.tag + stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) + [stack.first].compact + end + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array + if !api_map.loose_unions && pin_groups.any? { |pins| pins.empty? } + pin_groups = [] end + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array + pins = pin_groups.flatten.uniq(&:path) + # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array return [] if pins.empty? inferred_pins(pins, api_map, name_pin, locals) end From 27bde68aaf7e3cc0380d04b4a446f9fc387cdb92 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 05:43:49 -0400 Subject: [PATCH 195/524] Unused argument --- lib/solargraph/api_map/constants.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index b9e7865d2..2080e1b4f 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -92,7 +92,7 @@ def resolve_uncached name, gates # @return [String, nil] def complex_resolve name, gates, internal resolved = nil - gates.each.with_index do |gate, idx| + gates.each do |gate| resolved = simple_resolve(name, gate, internal) return resolved if resolved store.get_ancestor_references(gate).each do |ref| From fe67c88823547655f30e931d369fc9b01bbaee0a Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 05:56:08 -0400 Subject: [PATCH 196/524] Revert method and parameter pin changes --- lib/solargraph/pin/method.rb | 2 +- lib/solargraph/pin/parameter.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index fe6e16e54..295b72fbf 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -556,7 +556,7 @@ def resolve_reference ref, api_map if parts.first.empty? || parts.one? path = "#{namespace}#{ref}" else - fqns = api_map.resolve(parts.first, *gates) + fqns = api_map.qualify(parts.first, *gates) return ComplexType::UNDEFINED if fqns.nil? path = fqns + ref[parts.first.length] + parts.last end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 3b4458183..947513689 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -251,7 +251,7 @@ def resolve_reference ref, api_map, skip if parts.first.empty? path = "#{namespace}#{ref}" else - fqns = api_map.resolve(parts.first, namespace) + fqns = api_map.qualify(parts.first, namespace) return nil if fqns.nil? path = fqns + ref[parts.first.length] + parts.last end From 3c5d3c97fd64cf6c0db32201b90e991385d3a1d0 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 07:34:45 -0400 Subject: [PATCH 197/524] Constants resumes open gates for deep name collisions --- lib/solargraph/api_map/constants.rb | 37 ++++++++++++++++++++--------- lib/solargraph/type_checker.rb | 2 +- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 2080e1b4f..a4c991f60 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -51,7 +51,16 @@ def qualify name, *gates return name if ['Boolean', 'self', nil].include?(name) gates.push '' unless gates.include?('') - resolve(name, gates) + fqns = resolve(name, gates) + return unless fqns + pin = store.get_path_pins(fqns).first + if pin.is_a?(Pin::Constant) + const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) + return unless const + resolve(const, pin.gates) + else + fqns + end end # @return [void] @@ -78,10 +87,15 @@ def resolve_uncached name, gates resolved = nil base = gates parts = name.split('::') + first = nil parts.each.with_index do |nam, idx| - resolved = complex_resolve(nam, base, idx != parts.length - 1) - break unless resolved - base = [resolved] + resolved, remainder = complex_resolve(nam, base, idx != parts.length - 1) + first ||= remainder + if resolved + base = [resolved] + else + return resolve(name, first) unless first.empty? + end end resolved end @@ -89,25 +103,26 @@ def resolve_uncached name, gates # @param name [String] # @param gates [Array] # @param internal [Boolean] True if the name is not the last in the namespace - # @return [String, nil] + # @return [Array] def complex_resolve name, gates, internal resolved = nil - gates.each do |gate| + gates.each.with_index do |gate, idx| resolved = simple_resolve(name, gate, internal) - return resolved if resolved - store.get_ancestor_references(gate).each do |ref| + return [resolved, gates[idx + 1..]] if resolved + (store.get_ancestor_references(gate)).each do |ref| mixin = resolve(ref.name, ref.reference_gates - [gate]) + next unless mixin resolved = simple_resolve(name, mixin, internal) - return resolved if resolved + return [resolved, gates[idx + 1..]] if resolved end end - nil + [nil, []] end # @param name [String] # @param gate [String] # @param internal [Boolean] True if the name is not the last in the namespace - # @return [Pin::Constant, Pin::Namespace, nil] + # @return [String, nil] def simple_resolve name, gate, internal here = "#{gate}::#{name}".sub(/^::/, '').sub(/::$/, '') pin = store.get_path_pins(here).first diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index b8bb6e82a..88190409a 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -96,7 +96,7 @@ def method_tag_problems def method_return_type_problems_for pin return [] if pin.is_a?(Pin::MethodAlias) result = [] - declared = pin.typify(api_map).self_to_type(pin.full_context).qualify(api_map, *pin.closure.gates) + declared = pin.typify(api_map).self_to_type(pin.full_context).qualify(api_map, *pin.gates) if declared.undefined? if pin.return_type.undefined? && rules.require_type_tags? if pin.attribute? From 67f651719a7a6051b1823c738650d484e87639fd Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 07:35:07 -0400 Subject: [PATCH 198/524] Typechecker specs --- spec/type_checker/levels/normal_spec.rb | 21 +++++++++++++++++++++ spec/type_checker/levels/typed_spec.rb | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/type_checker/levels/normal_spec.rb b/spec/type_checker/levels/normal_spec.rb index 3b38f55d8..9af91efe1 100644 --- a/spec/type_checker/levels/normal_spec.rb +++ b/spec/type_checker/levels/normal_spec.rb @@ -909,5 +909,26 @@ def get_a_mutex; end )) expect(checker.problems).to be_empty end + + it 'resolves namespace gate conflicts' do + checker = type_checker(%( + class Base + class Target + end + end + + module Other + class Base + end + + class Deep + # @return [Base::Target] + def foo + end + end + end + )) + expect(checker.problems).to be_empty + end end end diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index b10bbd42c..b2071465e 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -388,7 +388,7 @@ def nil_assignment? 'foo'.nil? # infers as 'false' end )) - expect(checker.problems.map(&:message)).to be_empty + expect(checker.problems).to be_empty end end end From af51344e07825c8e363f9c53a5732e83f1f7b5a0 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 07:42:02 -0400 Subject: [PATCH 199/524] Linting --- lib/solargraph/api_map/constants.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index a4c991f60..0c83d967d 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -109,7 +109,7 @@ def complex_resolve name, gates, internal gates.each.with_index do |gate, idx| resolved = simple_resolve(name, gate, internal) return [resolved, gates[idx + 1..]] if resolved - (store.get_ancestor_references(gate)).each do |ref| + store.get_ancestor_references(gate).each do |ref| mixin = resolve(ref.name, ref.reference_gates - [gate]) next unless mixin resolved = simple_resolve(name, mixin, internal) From 9009a2ca34c39cf42b7aa433e1cfbfff226dbfe0 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 07:58:48 -0400 Subject: [PATCH 200/524] Documentation --- lib/solargraph/api_map.rb | 2 ++ lib/solargraph/api_map/constants.rb | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f233ecb74..44ca19035 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -297,6 +297,8 @@ def qualify tag, *gates store.constants.qualify(tag, *gates) end + # @see Store::Constants#resolve + # # @param name [String] # @param gates [Array>] # @return [String, nil] diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 0c83d967d..2e38b9ad6 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -12,6 +12,11 @@ def initialize store # Resolve a name to a fully qualified namespace or constant. # + # `Constants#resolve` is similar to `Constants#qualify`` in that its + # purpose is to find fully qualified (absolute) namespaces, except + # `#resolve`` is only concerned with real namespaces. It disregards + # parametrized types and special types like literals, self, and Boolean. + # # @param name [String] # @param gates [Array, String>] # @return [String, nil] @@ -108,12 +113,12 @@ def complex_resolve name, gates, internal resolved = nil gates.each.with_index do |gate, idx| resolved = simple_resolve(name, gate, internal) - return [resolved, gates[idx + 1..]] if resolved + return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| mixin = resolve(ref.name, ref.reference_gates - [gate]) next unless mixin resolved = simple_resolve(name, mixin, internal) - return [resolved, gates[idx + 1..]] if resolved + return [resolved, gates[(idx + 1)..]] if resolved end end [nil, []] From 8e171845184a82d51fb8085c32ae3f2d1bd9a5d1 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 08:05:51 -0400 Subject: [PATCH 201/524] Typechecking --- lib/solargraph/api_map/constants.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 2e38b9ad6..333b5f8fb 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -105,10 +105,14 @@ def resolve_uncached name, gates resolved end + # @todo I'm not sure of a better way to express the return value in YARD. + # It's a tuple where the first element is a nullable string. Something + # like `Array(String|nil, Array)` would be more accurate. + # # @param name [String] # @param gates [Array] # @param internal [Boolean] True if the name is not the last in the namespace - # @return [Array] + # @return [Array(Object, Array)] def complex_resolve name, gates, internal resolved = nil gates.each.with_index do |gate, idx| From ed3809f4695a3555892a84a48f36d11328de9003 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 09:43:27 -0400 Subject: [PATCH 202/524] Fix ancestral recursion --- lib/solargraph/api_map/constants.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 333b5f8fb..552a6b04e 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -119,7 +119,8 @@ def complex_resolve name, gates, internal resolved = simple_resolve(name, gate, internal) return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| - mixin = resolve(ref.name, ref.reference_gates - [gate]) + return ref.name if ref.name.end_with?("::#{name}") + mixin = resolve(ref.name, ref.reference_gates - gates) next unless mixin resolved = simple_resolve(name, mixin, internal) return [resolved, gates[(idx + 1)..]] if resolved From 1864b67a3818113bdb8d108c8978ac4b5c48e783 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Thu, 18 Sep 2025 10:22:20 -0400 Subject: [PATCH 203/524] Constants strips leading namespace separators --- lib/solargraph/api_map/constants.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 552a6b04e..430303ae1 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -119,7 +119,7 @@ def complex_resolve name, gates, internal resolved = simple_resolve(name, gate, internal) return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| - return ref.name if ref.name.end_with?("::#{name}") + return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") mixin = resolve(ref.name, ref.reference_gates - gates) next unless mixin resolved = simple_resolve(name, mixin, internal) From e5d88a51307a948de85880fcbbde4a8ec790d199 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 19 Sep 2025 18:15:17 -0400 Subject: [PATCH 204/524] Show all evaluated types in typechecker --- lib/solargraph/type_checker.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index c315a407f..9b98d3d74 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -295,15 +295,20 @@ def call_problems if type.undefined? && !rules.ignore_all_undefined? base = chain missing = chain + # @type [Solargraph::Pin::Base, nil] found = nil + # @type [Array] + all_found = [] closest = ComplexType::UNDEFINED until base.links.first.undefined? - found = base.define(api_map, block_pin, locals).first + all_found = base.define(api_map, block_pin, locals) + found = all_found.first break if found missing = base base = base.base end - closest = found.typify(api_map) if found + all_closest = all_found.map { |pin| pin.typify(api_map) } + closest = ComplexType.new(all_closest.flat_map(&:items).uniq) # @todo remove the internal_or_core? check at a higher-than-strict level if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) unless closest.generic? || ignored_pins.include?(found) @@ -603,15 +608,20 @@ def declared_externally? pin if type.undefined? && !rules.ignore_all_undefined? base = chain missing = chain + # @type [Solargraph::Pin::Base, nil] found = nil + # @type [Array] + all_found = [] closest = ComplexType::UNDEFINED until base.links.first.undefined? - found = base.define(api_map, block_pin, locals).first + all_found = base.define(api_map, block_pin, locals) + found = all_found.first break if found missing = base base = base.base end - closest = found.typify(api_map) if found + all_closest = all_found.map { |pin| pin.typify(api_map) } + closest = ComplexType.new(all_closest.flat_map(&:items).uniq) if !found || closest.defined? || internal?(found) return false end From 62a1c97ee5b495ad0dba89d2841679d36d4e3580 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 21 Sep 2025 12:32:53 -0400 Subject: [PATCH 205/524] Less-rigorous assertions merging assignment attribute of parameters Default values don't exist in RBS; it just tells you if the arg is optinal or not - this was a problem in solargraph-rails specs while assertions were on. --- lib/solargraph/pin/local_variable.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 78e0b287d..512231a8a 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -23,14 +23,22 @@ def initialize assignment: nil, presence: nil, presence_certain: false, **splat def combine_with(other, attrs={}) new_attrs = { - assignment: assert_same(other, :assignment), + # default values don't exist in RBS; it just tells you if + # the arg is optinal or not; prefer a provided value if we + # have one here, but don't be too picky otherwise, as it's + # ultimately just documentation, not something we use for + # inference or typechecking + # + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + assignment: choose(other, :assignment), + + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 presence_certain: assert_same(other, :presence_certain?), }.merge(attrs) - # @sg-ignore Wrong argument type for - # Solargraph::Pin::Base#assert_same: other expected - # Solargraph::Pin::Base, received self + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end From 982ad1a74096ec5f5cf817d9b715ffeda7837faf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 22 Sep 2025 11:07:47 -0400 Subject: [PATCH 206/524] Remove duplicate assignment handling --- lib/solargraph/pin/base_variable.rb | 8 +++++++- lib/solargraph/pin/local_variable.rb | 13 +------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 764c1fb39..93845b585 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -23,7 +23,13 @@ def initialize assignment: nil, return_type: nil, **splat def combine_with(other, attrs={}) attrs.merge({ - assignment: assert_same(other, :assignment), + # default values don't exist in RBS parameters; it just + # tells you if the arg is optional or not. Prefer a + # provided value if we have one here since we can't rely on + # it from RBS so we can infer from it and typecheck on it. + # + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + assignment: choose(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), }) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 512231a8a..db6dcfe6a 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -10,28 +10,17 @@ def presence_certain? @presence_certain end - # @param assignment [AST::Node, nil] # @param presence [Range, nil] # @param presence_certain [Boolean] # @param splat [Hash] - def initialize assignment: nil, presence: nil, presence_certain: false, **splat + def initialize presence: nil, presence_certain: false, **splat super(**splat) - @assignment = assignment @presence = presence @presence_certain = presence_certain end def combine_with(other, attrs={}) new_attrs = { - # default values don't exist in RBS; it just tells you if - # the arg is optinal or not; prefer a provided value if we - # have one here, but don't be too picky otherwise, as it's - # ultimately just documentation, not something we use for - # inference or typechecking - # - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 - assignment: choose(other, :assignment), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 presence_certain: assert_same(other, :presence_certain?), }.merge(attrs) From 3399448f739d09b0a1297e878fac5499ff9bb816 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 23 Sep 2025 20:59:58 -0400 Subject: [PATCH 207/524] Add @sg-ignore --- lib/solargraph/api_map/store.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 5978e322d..91db664b5 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -297,6 +297,10 @@ def fqns_pins_map end end + # @sg-ignore Rooted type issue here - "Declared return type + # ::Enumerable<::Solargraph::Pin::Symbol> does not match + # inferred type ::Set<::Symbol> for + # Solargraph::ApiMap::Store#symbols" # @return [Enumerable] def symbols index.pins_by_class(Pin::Symbol) From 4eda431ce26211bf4d6955cbae997b0e79ffe4d4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 24 Sep 2025 14:56:12 -0400 Subject: [PATCH 208/524] Handle RBS static method aliases Should fix solargraph-rails spec failures --- lib/solargraph/api_map.rb | 2 +- spec/rbs_map/conversions_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 44ca19035..e176a10cd 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -860,7 +860,7 @@ def prefer_non_nil_variables pins # @param alias_pin [Pin::MethodAlias] # @return [Pin::Method, nil] def resolve_method_alias(alias_pin) - ancestors = store.get_ancestors(alias_pin.full_context.tag) + ancestors = store.get_ancestors(alias_pin.full_context.reduce_class_type.tag) original = nil # Search each ancestor for the original method diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 1df43af26..8afdeca2d 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -25,6 +25,29 @@ attr_reader :temp_dir + context 'with self alias to self method' do + let(:rbs) do + <<~RBS + class Foo + def self.bar: () -> String + alias self.bar? self.bar + end + RBS + end + + subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } + + let(:method_pin) { api_map.get_method_stack('Foo', 'bar', scope: :class).first } + + it { should_not be_nil } + + it { should be_instance_of(Solargraph::Pin::Method) } + + it 'finds the type' do + expect(alias_pin.return_type.tag).to eq('String') + end + end + context 'with untyped response' do let(:rbs) do <<~RBS From 037307ed9fc9945810c64ce867c9e09a4003cd7c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 24 Sep 2025 15:17:37 -0400 Subject: [PATCH 209/524] Fix #reduce_class_type --- lib/solargraph/complex_type.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 8798ecb88..669a66900 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -246,6 +246,7 @@ def all_params def reduce_class_type new_items = items.flat_map do |type| next type unless ['Module', 'Class'].include?(type.name) + next type if type.all_params.empty? type.all_params end From 25b0770551d5b5c9a702dfbfc90dcd7f01685c1a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 24 Sep 2025 15:20:10 -0400 Subject: [PATCH 210/524] linting --- spec/rbs_map/conversions_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 8afdeca2d..d1d3e564d 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -35,10 +35,10 @@ def self.bar: () -> String RBS end - subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } - let(:method_pin) { api_map.get_method_stack('Foo', 'bar', scope: :class).first } + subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } + it { should_not be_nil } it { should be_instance_of(Solargraph::Pin::Method) } From 6a7a37feedad44e0be8bf0fba8c5f9d4e9b83964 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 24 Sep 2025 16:49:01 -0400 Subject: [PATCH 211/524] Fix RuboCop issues --- spec/rbs_map/conversions_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 4ed3f511d..31c354023 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -52,6 +52,8 @@ class C < ::B::C end context 'with self alias to self method' do + subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } + let(:rbs) do <<~RBS class Foo @@ -63,11 +65,9 @@ def self.bar: () -> String let(:method_pin) { api_map.get_method_stack('Foo', 'bar', scope: :class).first } - subject(:alias_pin) { api_map.get_method_stack('Foo', 'bar?', scope: :class).first } - - it { should_not be_nil } + it { is_expected.not_to be_nil } - it { should be_instance_of(Solargraph::Pin::Method) } + it { is_expected.to be_instance_of(Solargraph::Pin::Method) } it 'finds the type' do expect(alias_pin.return_type.tag).to eq('String') From a398b84bd2f09c4e9148aee79c8ee0a643c651a1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 12:45:46 -0400 Subject: [PATCH 212/524] Resolve constants in references Also replaces 'include' logic with call to ApiMap::Constants Fixes #1099 --- lib/solargraph/api_map.rb | 13 +--- lib/solargraph/api_map/constants.rb | 23 ++++--- lib/solargraph/parser/node_methods.rb | 97 --------------------------- spec/api_map/constants_spec.rb | 27 ++++++++ 4 files changed, 44 insertions(+), 116 deletions(-) delete mode 100644 lib/solargraph/parser/node_methods.rb diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 44ca19035..7f53e7596 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -772,17 +772,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| - const = get_constants('', *ref.closure.gates).find { |pin| pin.path.end_with? ref.name } - if const.is_a?(Pin::Namespace) - result.concat inner_get_methods(const.path, scope, visibility, deep, skip, true) - elsif const.is_a?(Pin::Constant) - type = const.infer(self) - result.concat inner_get_methods(type.namespace, scope, visibility, deep, skip, true) if type.defined? - else - referenced_tag = ref.parametrized_tag - next unless referenced_tag.defined? - result.concat inner_get_methods_from_reference(referenced_tag.to_s, namespace_pin, rooted_type, scope, visibility, deep, skip, true) - end + fqin = dereference(ref) + result.concat inner_get_methods(fqin, scope, visibility, deep, skip, true) end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 430303ae1..5dbcd4b67 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -12,14 +12,21 @@ def initialize store # Resolve a name to a fully qualified namespace or constant. # - # `Constants#resolve` is similar to `Constants#qualify`` in that its - # purpose is to find fully qualified (absolute) namespaces, except - # `#resolve`` is only concerned with real namespaces. It disregards - # parametrized types and special types like literals, self, and Boolean. + # `Constants#resolve` finds fully qualified (absolute) + # namespaces based on relative names and the open gates + # (namespaces) provided. Names must be runtime-visible (erased) + # non-literal types - e.g., TrueClass, NilClass, Integer and + # Hash instead of true, nil, 96, or Hash{String => Symbol} # - # @param name [String] - # @param gates [Array, String>] - # @return [String, nil] + # Note: You may want to be using #qualify. Notably, #resolve: + # - will not gracefully handle nil, self and Boolean + # - will return a constant name instead of following its assignment + # + # @param name [String] Namespace which may relative and not be rooted. + # @param gates [Array, String>] Namespaces to search while resolving the name + # + # @return [String, nil] fully qualified namespace (i.e., is + # absolute, but will not start with ::) def resolve(name, *gates) return store.get_path_pins(name[2..]).first&.path if name.start_with?('::') @@ -33,7 +40,7 @@ def resolve(name, *gates) # @param pin [Pin::Reference] # @return [String, nil] def dereference pin - resolve(pin.name, pin.reference_gates) + qualify(pin.name, pin.reference_gates) end # Collect a list of all constants defined in the specified gates. diff --git a/lib/solargraph/parser/node_methods.rb b/lib/solargraph/parser/node_methods.rb deleted file mode 100644 index f33a924c1..000000000 --- a/lib/solargraph/parser/node_methods.rb +++ /dev/null @@ -1,97 +0,0 @@ -module Solargraph - module Parser - module NodeMethods - module_function - - # @abstract - # @param node [Parser::AST::Node] - # @return [String] - def unpack_name node - raise NotImplementedError - end - - # @abstract - # @todo Temporarily here for testing. Move to Solargraph::Parser. - # @param node [Parser::AST::Node] - # @return [Array] - def call_nodes_from node - raise NotImplementedError - end - - # Find all the nodes within the provided node that potentially return a - # value. - # - # The node parameter typically represents a method's logic, e.g., the - # second child (after the :args node) of a :def node. A simple one-line - # method would typically return itself, while a node with conditions - # would return the resulting node from each conditional branch. Nodes - # that follow a :return node are assumed to be unreachable. Nil values - # are converted to nil node types. - # - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] - def returns_from_method_body node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # - # @return [Array] - def const_nodes_from node - raise NotImplementedError - end - - # @abstract - # @param cursor [Solargraph::Source::Cursor] - # @return [Parser::AST::Node, nil] - def find_recipient_node cursor - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Array] low-level value nodes in - # value position. Does not include explicit return - # statements - def value_position_nodes_only(node) - raise NotImplementedError - end - - # @abstract - # @param nodes [Enumerable] - def any_splatted_call?(nodes) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [void] - def process node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Hash{Symbol => Source::Chain}] - def convert_hash node - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_start_position(node) - raise NotImplementedError - end - - # @abstract - # @param node [Parser::AST::Node] - # @return [Position] - def get_node_end_position(node) - raise NotImplementedError - end - end - end -end diff --git a/spec/api_map/constants_spec.rb b/spec/api_map/constants_spec.rb index 26eaf6b25..e09c12c29 100644 --- a/spec/api_map/constants_spec.rb +++ b/spec/api_map/constants_spec.rb @@ -20,6 +20,33 @@ module Quuz expect(resolved).to eq('Foo::Bar') end + it 'resolves constants in includes' do + code = %( + module A + module Parser + module C + module_function + + # @return [String] + def baz; "abc"; end + end + + B = C + end + + class Foo + include Parser::B + + # @return [String] + def bar + baz + end + end + end) + checker = Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) + expect(checker.problems.map(&:message)).to be_empty + end + it 'returns namespaces for nested namespaces' do source_map = Solargraph::SourceMap.load_string(%( module Foo From dd06a479b9f12e797b12caef53bdff482f686426 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 12:51:45 -0400 Subject: [PATCH 213/524] Linting --- spec/api_map/constants_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/api_map/constants_spec.rb b/spec/api_map/constants_spec.rb index e09c12c29..ef07c00b7 100644 --- a/spec/api_map/constants_spec.rb +++ b/spec/api_map/constants_spec.rb @@ -25,8 +25,6 @@ module Quuz module A module Parser module C - module_function - # @return [String] def baz; "abc"; end end From cc86aec403ca74694a408e85e6ccd1f5f5e32343 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 13:00:55 -0400 Subject: [PATCH 214/524] Ratchet RuboCop --- .rubocop_todo.yml | 35 +---------------------------------- lib/solargraph/api_map.rb | 2 -- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 89fd47c5d..0de27608a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.80.2. +# using RuboCop version 1.80.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -280,7 +280,6 @@ Layout/MultilineMethodCallBraceLayout: # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/diagnostics/type_check.rb' - 'lib/solargraph/language_server/message/completion_item/resolve.rb' - 'lib/solargraph/language_server/message/text_document/hover.rb' @@ -356,7 +355,6 @@ Layout/SpaceBeforeBlockBraces: - 'lib/solargraph/source.rb' - 'lib/solargraph/source/chain/call.rb' - 'lib/solargraph/source/chain/class_variable.rb' - - 'lib/solargraph/source/chain/constant.rb' - 'lib/solargraph/source/chain/global_variable.rb' - 'lib/solargraph/source/chain/instance_variable.rb' - 'lib/solargraph/source/chain/variable.rb' @@ -433,13 +431,11 @@ Layout/TrailingWhitespace: Exclude: - 'lib/solargraph/language_server/message/client/register_capability.rb' - 'spec/api_map/config_spec.rb' - - 'spec/convention_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' # This cop supports safe autocorrection (--autocorrect). @@ -512,11 +508,6 @@ Lint/DuplicateMethods: - 'lib/solargraph/rbs_map/core_map.rb' - 'lib/solargraph/source/chain/link.rb' -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/convention_spec.rb' - # Configuration parameters: AllowComments. Lint/EmptyClass: Exclude: @@ -998,11 +989,6 @@ RSpec/DescribedClass: - 'spec/yard_map/mapper/to_method_spec.rb' - 'spec/yard_map/mapper_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/convention_spec.rb' - # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterFinalLet: Exclude: @@ -1321,7 +1307,6 @@ Style/AccessorGrouping: # SupportedStyles: always, conditionals Style/AndOr: Exclude: - - 'lib/solargraph/api_map/source_to_yard.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/language_server/message/base.rb' - 'lib/solargraph/page.rb' @@ -1510,7 +1495,6 @@ Style/Documentation: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/parser_gem/class_methods.rb' @@ -1651,7 +1635,6 @@ Style/FrozenStringLiteralComment: - 'lib/solargraph/parser.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/parser_gem.rb' - 'lib/solargraph/parser/snippet.rb' - 'lib/solargraph/pin/breakable.rb' @@ -1782,7 +1765,6 @@ Style/GlobalStdStream: # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/library.rb' - 'lib/solargraph/parser/parser_gem/node_processors/send_node.rb' - 'lib/solargraph/pin_cache.rb' @@ -1835,7 +1817,6 @@ Style/IfInsideElse: # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' @@ -1903,7 +1884,6 @@ Style/MethodDefParentheses: Exclude: - 'lib/solargraph.rb' - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/type_methods.rb' @@ -1925,7 +1905,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/location.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/parser/flow_sensitive_typing.rb' - - 'lib/solargraph/parser/node_methods.rb' - 'lib/solargraph/parser/node_processor/base.rb' - 'lib/solargraph/parser/parser_gem/flawed_builder.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' @@ -1947,7 +1926,6 @@ Style/MethodDefParentheses: - 'lib/solargraph/shell.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/source/chain/call.rb' - - 'lib/solargraph/source/chain/constant.rb' - 'lib/solargraph/source_map.rb' - 'lib/solargraph/source_map/mapper.rb' - 'lib/solargraph/type_checker.rb' @@ -2049,8 +2027,6 @@ Style/NumericLiterals: Style/NumericPredicate: Exclude: - 'spec/**/*' - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/type_methods.rb' @@ -2131,7 +2107,6 @@ Style/RedundantFreeze: # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: - - 'lib/solargraph/api_map/store.rb' - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/source_map/mapper.rb' @@ -2143,7 +2118,6 @@ Style/RedundantParentheses: - 'lib/solargraph/parser/parser_gem/node_chainer.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/parameter.rb' - - 'lib/solargraph/pin/search.rb' - 'lib/solargraph/source.rb' - 'lib/solargraph/type_checker.rb' @@ -2170,7 +2144,6 @@ Style/RedundantRegexpEscape: # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type/type_methods.rb' - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/parser/parser_gem/node_methods.rb' @@ -2269,8 +2242,6 @@ Style/StderrPuts: # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/pin/base.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/closure.rb' @@ -2286,7 +2257,6 @@ Style/StringLiterals: Exclude: - 'Gemfile' - 'Rakefile' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/complex_type.rb' - 'lib/solargraph/complex_type/unique_type.rb' - 'lib/solargraph/convention/struct_definition.rb' @@ -2504,8 +2474,6 @@ Style/YAMLFileRead: # This cop supports unsafe autocorrection (--autocorrect-all). Style/ZeroLengthPredicate: Exclude: - - 'lib/solargraph/api_map.rb' - - 'lib/solargraph/api_map/index.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/source/chain/array.rb' @@ -2614,7 +2582,6 @@ Layout/LineLength: - 'lib/solargraph/workspace.rb' - 'lib/solargraph/workspace/config.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'spec/api_map_spec.rb' - 'spec/complex_type_spec.rb' - 'spec/language_server/message/completion_item/resolve_spec.rb' - 'spec/language_server/message/extended/check_gem_version_spec.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 7f53e7596..7bac1395c 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -737,7 +737,6 @@ def store # @param skip [Set] # @param no_core [Boolean] Skip core classes if true # @return [Array] - # rubocop:disable Metrics/CyclomaticComplexity def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false rooted_type = ComplexType.parse(rooted_tag).force_rooted fqns = rooted_type.namespace @@ -802,7 +801,6 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end result end - # rubocop:enable Metrics/CyclomaticComplexity # @return [Hash] def path_macros From 343cd23307e00ae0c0d4952514f326eac209de5b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 14:36:29 -0400 Subject: [PATCH 215/524] Fix lack of parameters in include types --- lib/solargraph/api_map.rb | 6 ++-- lib/solargraph/api_map/constants.rb | 37 +++++++++++++-------- lib/solargraph/api_map/store.rb | 2 +- lib/solargraph/complex_type/type_methods.rb | 4 +++ lib/solargraph/pin/reference.rb | 13 ++------ 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 7bac1395c..14d33b1d7 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -771,8 +771,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if scope == :instance store.get_includes(fqns).reverse.each do |ref| - fqin = dereference(ref) - result.concat inner_get_methods(fqin, scope, visibility, deep, skip, true) + in_tag = dereference(ref) + result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true) end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? @@ -781,7 +781,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false else 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 = store.constants.dereference(em) + fqem = dereference(em) result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil? end rooted_sc_tag = qualify_superclass(rooted_tag) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 5dbcd4b67..0b6070507 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -15,10 +15,12 @@ def initialize store # `Constants#resolve` finds fully qualified (absolute) # namespaces based on relative names and the open gates # (namespaces) provided. Names must be runtime-visible (erased) - # non-literal types - e.g., TrueClass, NilClass, Integer and - # Hash instead of true, nil, 96, or Hash{String => Symbol} + # non-literal types, non-duck, non-signature types - e.g., + # TrueClass, NilClass, Integer and Hash instead of true, nil, + # 96, or Hash{String => Symbol} # # Note: You may want to be using #qualify. Notably, #resolve: + # - does not handle anything with type parameters # - will not gracefully handle nil, self and Boolean # - will return a constant name instead of following its assignment # @@ -40,7 +42,7 @@ def resolve(name, *gates) # @param pin [Pin::Reference] # @return [String, nil] def dereference pin - qualify(pin.name, pin.reference_gates) + qualify_type(pin.type, pin.reference_gates)&.tag end # Collect a list of all constants defined in the specified gates. @@ -52,27 +54,36 @@ def collect(*gates) cached_collect[flat] || collect_and_cache(flat) end - # Determine a fully qualified namespace for a given name referenced - # from the specified open gates. This method will search in each gate - # until it finds a match for the name. + # Determine a fully qualified namespace for a given tag + # referenced from the specified open gates. This method will + # search in each gate until it finds a match for the name. # - # @param name [String, nil] The namespace to match + # @param tag [String, nil] The type to match # @param gates [Array] # @return [String, nil] fully qualified tag - def qualify name, *gates - return name if ['Boolean', 'self', nil].include?(name) + def qualify tag, *gates + type = ComplexType.try_parse(tag) + qualify_type(type)&.tag + end + + # @param type [ComplexType, nil] The type to match + # @param gates [Array] + # + # @return [ComplexType, nil] A new rooted ComplexType + def qualify_type type, *gates + return nil if type.nil? + return type if type.selfy? || type.literal? || type.tag == 'nil' || type.interface? gates.push '' unless gates.include?('') - fqns = resolve(name, gates) + fqns = resolve(type.namespace, type.namespace) return unless fqns pin = store.get_path_pins(fqns).first if pin.is_a?(Pin::Constant) const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) return unless const - resolve(const, pin.gates) - else - fqns + fqns = resolve(const, pin.gates) end + type.recreate(new_name: fqns, rooted: true) end # @return [void] diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index b3953cfec..a0b2ae856 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -244,7 +244,7 @@ def get_ancestors(fqns) # Add includes, prepends, and extends [get_includes(current), get_prepends(current), get_extends(current)].each do |refs| next if refs.nil? - refs.map(&:parametrized_tag).map(&:to_s).each do |ref| + refs.map(&:type).map(&:to_s).each do |ref| next if ref.nil? || ref.empty? || visited.include?(ref) ancestors << ref queue << ref diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index d8d4fc7d7..1145bd034 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -43,6 +43,10 @@ def rooted_tag @rooted_tag ||= rooted_name + rooted_substring end + def interface? + name.start_with?('_') + end + # @return [Boolean] def duck_type? @duck_type ||= name.start_with?('#') diff --git a/lib/solargraph/pin/reference.rb b/lib/solargraph/pin/reference.rb index d678ab7b7..d456fbbf8 100644 --- a/lib/solargraph/pin/reference.rb +++ b/lib/solargraph/pin/reference.rb @@ -18,18 +18,9 @@ def initialize generic_values: [], **splat @generic_values = generic_values end - # @return [String] - def parameter_tag - @parameter_tag ||= if generic_values&.any? - "<#{generic_values.join(', ')}>" - else - '' - end - end - # @return [ComplexType] - def parametrized_tag - @parametrized_tag ||= ComplexType.try_parse( + def type + @type ||= ComplexType.try_parse( name + if generic_values&.length&.> 0 "<#{generic_values.join(', ')}>" From d787886b956aa9b5d748fe4003f0ceb1b4dcd4ca Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 15:16:22 -0400 Subject: [PATCH 216/524] Bug fixes --- lib/solargraph/api_map.rb | 2 +- lib/solargraph/api_map/constants.rb | 13 +++++++------ lib/solargraph/api_map/source_to_yard.rb | 4 ++-- lib/solargraph/api_map/store.rb | 2 +- lib/solargraph/complex_type.rb | 15 +++++++++++++++ .../convention/active_support_concern.rb | 2 +- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 14d33b1d7..286761db2 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -662,7 +662,7 @@ def super_and_sub?(sup, sub) # # @return [Boolean] def type_include?(host_ns, module_ns) - store.get_includes(host_ns).map { |inc_tag| inc_tag.parametrized_tag.name }.include?(module_ns) + store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns) end # @param pins [Enumerable] diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 0b6070507..9463e2037 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -42,7 +42,7 @@ def resolve(name, *gates) # @param pin [Pin::Reference] # @return [String, nil] def dereference pin - qualify_type(pin.type, pin.reference_gates)&.tag + qualify_type(pin.type, *pin.reference_gates)&.tag end # Collect a list of all constants defined in the specified gates. @@ -63,7 +63,7 @@ def collect(*gates) # @return [String, nil] fully qualified tag def qualify tag, *gates type = ComplexType.try_parse(tag) - qualify_type(type)&.tag + qualify_type(type, *gates)&.tag end # @param type [ComplexType, nil] The type to match @@ -72,18 +72,19 @@ def qualify tag, *gates # @return [ComplexType, nil] A new rooted ComplexType def qualify_type type, *gates return nil if type.nil? - return type if type.selfy? || type.literal? || type.tag == 'nil' || type.interface? + return type if type.selfy? || type.literal? || type.tag == 'nil' || type.interface? || + type.tag == 'Boolean' gates.push '' unless gates.include?('') - fqns = resolve(type.namespace, type.namespace) + fqns = resolve(type.rooted_namespace, *gates) return unless fqns pin = store.get_path_pins(fqns).first if pin.is_a?(Pin::Constant) const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment) return unless const - fqns = resolve(const, pin.gates) + fqns = resolve(const, *pin.gates) end - type.recreate(new_name: fqns, rooted: true) + type.recreate(new_name: fqns, make_rooted: true) end # @return [void] diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index ccbed3eb6..39d86a85c 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -46,13 +46,13 @@ def rake_yard store store.get_includes(pin.path).each do |ref| include_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) unless include_object.nil? || include_object.nil? - include_object.instance_mixins.push code_object_map[ref.parametrized_tag.to_s] + include_object.instance_mixins.push code_object_map[ref.type.to_s] end end store.get_extends(pin.path).each do |ref| extend_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject) next unless extend_object - code_object = code_object_map[ref.parametrized_tag.to_s] + code_object = code_object_map[ref.type.to_s] next unless code_object extend_object.class_mixins.push code_object # @todo add spec showing why this next line is necessary diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index a0b2ae856..d97d1d342 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -97,7 +97,7 @@ def qualify_superclass fq_sub_tag return unless ref res = constants.dereference(ref) return unless res - res + type.substring + res end # @param fqns [String] diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 8798ecb88..db378743f 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -105,6 +105,21 @@ def can_assign?(api_map, atype) any? { |ut| ut.can_assign?(api_map, atype) } end + # @param new_name [String, nil] + # @param make_rooted [Boolean, nil] + # @param new_key_types [Array, nil] + # @param rooted [Boolean, nil] + # @param new_subtypes [Array, nil] + # @return [self] + def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil) + ComplexType.new(map do |ut| + ut.recreate(new_name: new_name, + make_rooted: make_rooted, + new_key_types: new_key_types, + new_subtypes: new_subtypes) + end) + end + # @return [Integer] def length @items.length diff --git a/lib/solargraph/convention/active_support_concern.rb b/lib/solargraph/convention/active_support_concern.rb index 74c9ce765..ed1fba175 100644 --- a/lib/solargraph/convention/active_support_concern.rb +++ b/lib/solargraph/convention/active_support_concern.rb @@ -80,7 +80,7 @@ def process_include include_tag "ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \ "Handling class include include_tag=#{include_tag}" end - module_extends = api_map.get_extends(rooted_include_tag).map(&:parametrized_tag).map(&:to_s) + module_extends = api_map.get_extends(rooted_include_tag).map(&:type).map(&:to_s) logger.debug do "ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \ "found module extends of #{rooted_include_tag}: #{module_extends}" From 63a53dbed3be0c68d0eac7a7a61dd031b6c8466d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 15:35:50 -0400 Subject: [PATCH 217/524] Add actual type for Mutexes --- lib/solargraph/language_server/progress.rb | 2 +- lib/solargraph/library.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/language_server/progress.rb b/lib/solargraph/language_server/progress.rb index 10900a37e..98b155714 100644 --- a/lib/solargraph/language_server/progress.rb +++ b/lib/solargraph/language_server/progress.rb @@ -134,7 +134,7 @@ def keep_alive host end end - # @return [Mutex] + # @return [Thread::Mutex] def mutex @mutex ||= Mutex.new end diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 9d5162431..bdbd1354f 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -522,7 +522,7 @@ def find_external_requires source_map @external_requires = nil end - # @return [Mutex] + # @return [Thread::Mutex] def mutex @mutex ||= Mutex.new end From 9d4ba443abbe73a9bbab2cc3179ecee6e54d7d46 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 16:25:40 -0400 Subject: [PATCH 218/524] Allow more valid method pin paths --- lib/solargraph/shell.rb | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index cb919476c..f9b655664 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -257,7 +257,21 @@ def list # @return [void] def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) - pins = api_map.get_path_pins path + is_method = path.include?('#') || path.include?('.') + if is_method && options[:stack] + scope, ns, meth = if path.include? '#' + [:instance, *path.split('#', 2)] + else + [:class, *path.split('.', 2)] + end + + # @sg-ignore Wrong argument type for + # Solargraph::ApiMap#get_method_stack: rooted_tag + # expected String, received Array + pins = api_map.get_method_stack(ns, meth, scope: scope) + else + pins = api_map.get_path_pins path + end references = {} pin = pins.first case pin @@ -265,19 +279,6 @@ def pin path $stderr.puts "Pin not found for path '#{path}'" exit 1 when Pin::Method - # @sg-ignore Unresolved call to options - if options[:stack] - scope, ns, meth = if path.include? '#' - [:instance, *path.split('#', 2)] - else - [:class, *path.split('.', 2)] - end - - # @sg-ignore Wrong argument type for - # Solargraph::ApiMap#get_method_stack: rooted_tag - # expected String, received Array - pins = api_map.get_method_stack(ns, meth, scope: scope) - end when Pin::Namespace # @sg-ignore Unresolved call to options if options[:references] From 2712e6624275f341fffcb03aa62c2da881c3feaf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 16:31:05 -0400 Subject: [PATCH 219/524] RuboCop fix --- lib/solargraph/shell.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index f9b655664..40410d909 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -278,7 +278,6 @@ def pin path when nil $stderr.puts "Pin not found for path '#{path}'" exit 1 - when Pin::Method when Pin::Namespace # @sg-ignore Unresolved call to options if options[:references] From 7bc2092b7844aaa78298f1d94d5f5e7e7cc84f1f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 16:36:10 -0400 Subject: [PATCH 220/524] Linting --- lib/solargraph/shell.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 40410d909..046a74296 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -258,6 +258,7 @@ def list def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) is_method = path.include?('#') || path.include?('.') + # @sg-ignore Unresolved call to options if is_method && options[:stack] scope, ns, meth = if path.include? '#' [:instance, *path.split('#', 2)] From 7546f7355534c70d3ae9ddbb863304d5be557d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sun, 21 Sep 2025 17:41:33 +0200 Subject: [PATCH 221/524] Enable solargraph-rspec tests --- .github/workflows/plugins.yml | 55 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..ded3be5a0 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -105,30 +105,37 @@ jobs: - name: Ensure specs still run run: bundle exec rake spec - # run_solargraph_rspec_specs: - # # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - name: clone https://github.com/lekemula/solargraph-rspec/ - # run: | - # cd .. - # git clone https://github.com/lekemula/solargraph-rspec.git - # cd solargraph-rspec - # - name: Set up Ruby - # uses: ruby/setup-ruby@v1 - # with: - # ruby-version: '3.0' - # bundler-cache: false - # - name: Install gems - # run: | - # cd ../solargraph-rspec - # echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - # bundle install - # - name: Run specs - # run: | - # cd ../solargraph-rspec - # bundle exec rake spec + run_solargraph_rspec_specs: + # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem + runs-on: ubuntu-latest + env: + SOLARGRAPH_CACHE: ${{ github.workspace }}/solargraph-rspec/vendor/solargraph/cache + steps: + - uses: actions/checkout@v3 + - name: clone https://github.com/lekemula/solargraph-rspec/ + run: | + cd .. + git clone https://github.com/lekemula/solargraph-rspec.git + cd solargraph-rspec + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - name: Install gems + run: | + cd ../solargraph-rspec + echo "gem 'solargraph', path: '../solargraph'" >> Gemfile + bundle install + - name: Solargraph generate RSpec gems YARD and RBS pins + run: | + cd ../solargraph-rspec + rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) + bundle exec solargraph gems $rspec_gems + - name: Run specs + run: | + cd ../solargraph-rspec + bundle exec rspec run_solargraph_rails_specs: # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem From 9c79e5b3a01ac3fd8d84b35b94e4cb4110f91b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Sun, 21 Sep 2025 19:40:15 +0200 Subject: [PATCH 222/524] Fix rspec gems specs --- .github/workflows/plugins.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index ded3be5a0..1c633fda0 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -109,7 +109,8 @@ jobs: # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem runs-on: ubuntu-latest env: - SOLARGRAPH_CACHE: ${{ github.workspace }}/solargraph-rspec/vendor/solargraph/cache + SOLARGRAPH_CACHE: ${{ github.workspace }}/../solargraph-rspec/vendor/solargraph/cache + BUNDLE_PATH: ${{ github.workspace }}/../solargraph-rspec/vendor/bundle steps: - uses: actions/checkout@v3 - name: clone https://github.com/lekemula/solargraph-rspec/ @@ -121,12 +122,17 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: true + bundler-cache: false - name: Install gems run: | cd ../solargraph-rspec echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - bundle install + bundle config path ${{ env.BUNDLE_PATH }} + bundle install --jobs 4 --retry 3 + - name: Configure .solargraph.yml + run: | + cd ../solargraph-rspec + cp .solargraph.yml.example .solargraph.yml - name: Solargraph generate RSpec gems YARD and RBS pins run: | cd ../solargraph-rspec @@ -135,7 +141,7 @@ jobs: - name: Run specs run: | cd ../solargraph-rspec - bundle exec rspec + bundle exec rspec --format progress run_solargraph_rails_specs: # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem From 1416e1d1e3f51c5c44dea3807796cbccccd870d3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 28 Sep 2025 12:08:05 -0400 Subject: [PATCH 223/524] Reduce number of build jobs for faster CI feedback This takes out some lower value combinations - ideally we could keep the number of jobs to <= 20, which is the max that GHA will run simultaneously here. --- .github/workflows/rspec.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..33d09b579 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -23,12 +23,26 @@ jobs: matrix: ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', 'head'] rbs-version: ['3.6.1', '3.9.4', '4.0.0.dev.4'] - # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: + # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 - ruby-version: '3.0' rbs-version: '3.9.4' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' + # only include the 3.1 variants we include later + - ruby-version: '3.1' + # only include the 3.2 variants we include later + - ruby-version: '3.2' + # only include the 3.3 variants we include later + - ruby-version: '3.3' + include: + - ruby-version: '3.1' + rbs_version: '3.6.1' + - ruby-version: '3.2' + rbs_version: '3.9.4' + - ruby-version: '3.3' + rbs_version: '4.0.0.dev.4' + steps: - uses: actions/checkout@v3 - name: Set up Ruby From f2abb735f39f7adb08d4a487dd4c6a1b76acbfd2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 28 Sep 2025 12:10:38 -0400 Subject: [PATCH 224/524] Fix punctuation --- .github/workflows/rspec.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 33d09b579..76003b412 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -37,12 +37,11 @@ jobs: - ruby-version: '3.3' include: - ruby-version: '3.1' - rbs_version: '3.6.1' + rbs-version: '3.6.1' - ruby-version: '3.2' - rbs_version: '3.9.4' + rbs-version: '3.9.4' - ruby-version: '3.3' - rbs_version: '4.0.0.dev.4' - + rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 07398d43f2baf1e5090d1715500b3303bbbc3665 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 07:27:13 -0400 Subject: [PATCH 225/524] Exempt a file hit by the unmerged YARD fix --- .rubocop_todo.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ecc699d97..321f8ae52 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -89,13 +89,6 @@ Layout/EmptyLineBetweenDefs: Layout/EmptyLines: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines @@ -1290,6 +1283,7 @@ YARD/TagTypeSyntax: Exclude: - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/parser/comment_ripper.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/type_checker.rb' # This cop supports safe autocorrection (--autocorrect). From eabe151b3dc4fce0600d0e5c62850907b74101c2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 10:41:55 -0400 Subject: [PATCH 226/524] Update rubocop todo --- .rubocop_todo.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4ab62a41f..7d2e33136 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -89,13 +88,6 @@ Layout/EmptyLineBetweenDefs: Layout/EmptyLines: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines @@ -1266,6 +1258,7 @@ YARD/TagTypeSyntax: Exclude: - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/parser/comment_ripper.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/type_checker.rb' # This cop supports safe autocorrection (--autocorrect). From ce3ce8c3ed11ec0031d53ad01163b43bc029d9eb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 10:51:49 -0400 Subject: [PATCH 227/524] Fix merge issue --- lib/solargraph/api_map.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index a7462fb50..695de0230 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -109,7 +109,6 @@ def catalog bench if recreate_docmap @doc_map = DocMap.new(unresolved_requires, [], bench.workspace, out: nil) # @todo Implement gem preferences - @gemspecs = @doc_map.workspace.gemspecs @unresolved_requires = @doc_map.unresolved_requires end @cache.clear if store.update(@@core_map.pins, @doc_map.pins, conventions_environ.pins, iced_pins, live_pins) From a6bdfd6336f0e12833fc7a52d87bc47eab39b14b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 10:57:41 -0400 Subject: [PATCH 228/524] Drop no-longer-needed @sg-ignores --- lib/solargraph/library.rb | 1 - lib/solargraph/yardoc.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 1faa6a9ba..a14dc40ee 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -609,7 +609,6 @@ def cache_next_gemspec report_cache_progress spec.name, pending kwargs = {} kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty? - # @sg-ignore Unresolved call to capture3 on Module _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s, **kwargs) if s.success? diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index ffe7da4c3..50f212f13 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -28,7 +28,6 @@ def build_docs gem_yardoc_path, yard_plugins, gemspec return end - # @sg-ignore stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) return if status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } From 94de7b5c34c1da22e694289ef4f2cb2c4ef7064e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 11:19:06 -0400 Subject: [PATCH 229/524] Ratchet RuboCop --- .rubocop_todo.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 38cf122ee..e05aa4ccf 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -88,13 +87,6 @@ Layout/EmptyLineBetweenDefs: Layout/EmptyLines: Enabled: false -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'lib/solargraph/rbs_map/core_map.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines From 3ab765d51bbe781a8c746005f476e01db49a0926 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 12:11:50 -0400 Subject: [PATCH 230/524] Drop unneeded @sg-ignores --- lib/solargraph/workspace/gemspecs.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index c42b2d843..38b46da30 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -38,7 +38,6 @@ def resolve_require require return nil if require.empty? return gemspecs_required_from_bundler if require == 'bundler/require' - # @sg-ignore Variable type could not be inferred for gemspec # @type [Gem::Specification, nil] gemspec = Gem::Specification.find_by_path(require) if gemspec.nil? @@ -50,7 +49,6 @@ def resolve_require require # See if we can make a good guess: potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) file = "lib/#{require}.rb" - # @sg-ignore Unresolved call to files gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } rescue Gem::MissingSpecError logger.debug do @@ -161,7 +159,6 @@ def gemspecs_required_from_external_bundle 'puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }' \ '.to_h.to_json }' ] - # @sg-ignore Unresolved call to capture3 on Module o, e, s = Open3.capture3(*cmd) if s.success? Solargraph.logger.debug "External bundle: #{o}" From 470c26e1c755eb5c0ebca75c2ead76c8651342e9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 14:20:32 -0400 Subject: [PATCH 231/524] Mark spec as now working --- spec/pin/combine_with_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/pin/combine_with_spec.rb b/spec/pin/combine_with_spec.rb index 38d45a3e1..cc80d76d5 100644 --- a/spec/pin/combine_with_spec.rb +++ b/spec/pin/combine_with_spec.rb @@ -9,7 +9,6 @@ end it 'combines return types with another method without type parameters' do - pending('logic being added to handle this case') pin1 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') pin2 = Solargraph::Pin::Method.new(name: 'foo', parameters: [], comments: '@return [Array]') combined = pin1.combine_with(pin2) From 06eade69c5654f7b20b739d4e781f0c78b2800e5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 16:49:13 -0400 Subject: [PATCH 232/524] Drop sg-ignores --- lib/solargraph/source/chain/call.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index b42d76cc0..22e1a8043 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -56,19 +56,15 @@ def resolve api_map, name_pin, locals [] end return inferred_pins(found, api_map, name_pin, locals) unless found.empty? - # @sg-ignore Unresolved call to map on void, ::Enumerator<::Solargraph::ComplexType::UniqueType> pin_groups = name_pin.binder.each_unique_type.map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) [stack.first].compact end - # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array if !api_map.loose_unions && pin_groups.any? { |pins| pins.empty? } pin_groups = [] end - # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array pins = pin_groups.flatten.uniq(&:path) - # @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array return [] if pins.empty? inferred_pins(pins, api_map, name_pin, locals) end From d486e6485d4cce9d182da079c588c3821c69cfa6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 22:36:37 -0400 Subject: [PATCH 233/524] Trim more matrix entries to make room for solargraph-rspec specs --- .github/workflows/rspec.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 76003b412..628bbc8ab 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,6 +35,8 @@ jobs: - ruby-version: '3.2' # only include the 3.3 variants we include later - ruby-version: '3.3' + # only include the 3.4 variants we include later + - ruby-version: '3.3' include: - ruby-version: '3.1' rbs-version: '3.6.1' @@ -42,6 +44,8 @@ jobs: rbs-version: '3.9.4' - ruby-version: '3.3' rbs-version: '4.0.0.dev.4' + - ruby-version: '3.4' + rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 56342d4a9ff7c1141372404883cd2ddd1f337de7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 22:50:42 -0400 Subject: [PATCH 234/524] Fix version number --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 628bbc8ab..cc4efda4b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -36,7 +36,7 @@ jobs: # only include the 3.3 variants we include later - ruby-version: '3.3' # only include the 3.4 variants we include later - - ruby-version: '3.3' + - ruby-version: '3.4' include: - ruby-version: '3.1' rbs-version: '3.6.1' From 94e650bf298482a79e6139eedd50a21916fd0c14 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 18:01:26 -0400 Subject: [PATCH 235/524] Annotation fixes --- lib/solargraph/doc_map.rb | 4 ++-- lib/solargraph/pin/base.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 322ab8970..5dcf28552 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -128,7 +128,7 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version + # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version def self.all_yard_gems_in_memory @yard_gems_in_memory ||= {} end @@ -138,7 +138,7 @@ def self.all_rbs_collection_gems_in_memory @rbs_collection_gems_in_memory ||= {} end - # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version + # @return [Hash{Array(String, String) => Array}] Indexed by gemspec name and version def yard_pins_in_memory self.class.all_yard_gems_in_memory end diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index d55ba79d8..f71d93be3 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -309,6 +309,7 @@ def assert_same_count(other, attr) # @param other [self] # @param attr [::Symbol] # + # @sg-ignore # @return [undefined] def assert_same(other, attr) if other.nil? From ac05669348b9e5518a2defd0c4fc207393567bf5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 18:57:44 -0400 Subject: [PATCH 236/524] Document method --- lib/solargraph/workspace/gemspecs.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 05acb56c9..03ecc6f0a 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -144,6 +144,9 @@ def only_runtime_dependencies gemspec gemspec.dependencies - gemspec.development_dependencies end + # Return the gems which would be required by Bundler.require + # + # @see https://bundler.io/guides/groups.html # @return [Array] def auto_required_gemspecs_from_bundler # @todo Handle projects with custom Bundler/Gemfile setups From 84fb04996c8d90ec445091b3c38b662bba869f92 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 19:34:39 -0400 Subject: [PATCH 237/524] Drop untested change --- lib/solargraph/workspace/gemspecs.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 03ecc6f0a..a956e1c5d 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -63,11 +63,6 @@ def resolve_require require end end end - # @todo the 'requires' provided in Environ is being used - # by plugins to pass gem names instead of require paths - # - need to expand Environ to provide a place to put gem - # names and get new plugins out before retiring this. - gemspec ||= find_gem(gem_name_guess) return nil if gemspec.nil? [gemspec_or_preference(gemspec)] From 4fb2b2c74fb54c5b9a598aa34756fa2336eac4ae Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 19:35:20 -0400 Subject: [PATCH 238/524] Fix typo in existing code --- lib/solargraph/workspace/gemspecs.rb | 4 +--- spec/workspace/gemspecs_resolve_require_spec.rb | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index a956e1c5d..ea21ef203 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -118,9 +118,7 @@ def gemspec_or_preference gemspec return gemspec unless preference_map.key?(gemspec.name) return gemspec if gemspec.version == preference_map[gemspec.name].version - # @todo this code is unused but broken - # @sg-ignore Unresolved call to by_path - change_gemspec_version gemspec, preference_map[by_path.name].version + change_gemspec_version gemspec, preference_map[gemspec.name].version end # @param gemspec [Gem::Specification] diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index 6ee0cecf0..b88bae269 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -261,8 +261,6 @@ def configure_bundler_spec stub_value end it 'returns the preferred gemspec' do - pending('https://github.com/castwide/solargraph/pull/1006') - gemspecs = described_class.new(dir_path, preferences: preferences) specs = gemspecs.resolve_require('backport') backport = specs.find { |spec| spec.name == 'backport' } @@ -281,8 +279,6 @@ def configure_bundler_spec stub_value end it 'returns the gemspec we do have' do - pending('https://github.com/castwide/solargraph/pull/1006') - gemspecs = described_class.new(dir_path, preferences: preferences) specs = gemspecs.resolve_require('backport') backport = specs.find { |spec| spec.name == 'backport' } From 88e1f7391e2a330185fe626de6ee6212e5076d48 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 21:36:50 -0400 Subject: [PATCH 239/524] Fix merge --- spec/yard_map/mapper_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 84d2db90c..14451b97f 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -64,12 +64,9 @@ def pins_with require expect(inc).to be_a(Solargraph::Pin::Reference::Include) end - it 'adds corect gates' do + it 'adds correct gates' do # Asssuming the ast gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('ast') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - pin = pins.find do |pin| + pin = pins_with('ast').find do |pin| pin.is_a?(Solargraph::Pin::Namespace) && pin.name == 'Mixin' && pin.closure.path == 'AST::Processor' end expect(pin.gates).to eq(['AST::Processor::Mixin', 'AST::Processor', 'AST', '']) From e25702f3c948c58e56870f104988ba2ddb9b31b6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 08:26:23 -0400 Subject: [PATCH 240/524] Back out spec --- spec/workspace/gemspecs_resolve_require_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index b88bae269..aa7b8d79d 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -217,7 +217,9 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/gem_tasks' } - it 'returns gems' do + xit 'returns gems' do + pending('improved logic for require lookups') + expect(specs&.map(&:name)).to include('bundler') end end From 0aefc35b6035601d04fef46b387f10a55a9ec25a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 08:44:31 -0400 Subject: [PATCH 241/524] Add more solargraph-rspec requires --- spec/doc_map_spec.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index fad6d3363..94a7ebf3b 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -42,13 +42,17 @@ # These are auto-required by solargraph-rspec in case the bundle # includes these gems. In our case, it doesn't! unprovided_solargraph_rspec_requires = [ - 'rspec-rails', 'actionmailer', + 'actionpack' 'activerecord', - 'shoulda-matchers', - 'rspec-sidekiq', + 'activesupport', 'airborne', - 'activesupport' + 'rspec-core', + 'rspec-expectations', + 'rspec-mocks', + 'rspec-rails', + 'rspec-sidekiq', + 'shoulda-matchers', ] expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) .to eq(['not_a_gem']) From 358ff37e529914ab15e1a91f18fc63e24b16be43 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 08:46:17 -0400 Subject: [PATCH 242/524] Fix syntax --- spec/doc_map_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 94a7ebf3b..d7f40f585 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -43,7 +43,7 @@ # includes these gems. In our case, it doesn't! unprovided_solargraph_rspec_requires = [ 'actionmailer', - 'actionpack' + 'actionpack', 'activerecord', 'activesupport', 'airborne', @@ -52,7 +52,7 @@ 'rspec-mocks', 'rspec-rails', 'rspec-sidekiq', - 'shoulda-matchers', + 'shoulda-matchers' ] expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) .to eq(['not_a_gem']) From ada4c1299de7118ca625e2ce43af86bbdbab39c8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 12:09:44 -0400 Subject: [PATCH 243/524] Use bundle information to resolve gemspecs --- lib/solargraph/rbs_map/core_map.rb | 4 - lib/solargraph/workspace/gemspecs.rb | 365 +++++++++++++----- spec/api_map_method_spec.rb | 9 +- spec/doc_map_spec.rb | 23 +- .../gemspecs_fetch_dependencies_spec.rb | 18 +- spec/workspace/gemspecs_find_gem_spec.rb | 2 - .../gemspecs_resolve_require_spec.rb | 8 +- spec/yard_map/mapper_spec.rb | 57 ++- 8 files changed, 312 insertions(+), 174 deletions(-) diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 8c3d7dbdd..48b8dcdc4 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -34,10 +34,6 @@ def pins @pins end - def loader - @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false)) - end - private # @return [RBS::EnvironmentLoader] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index ea21ef203..55dac6c6a 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -43,29 +43,41 @@ def resolve_require require # @todo handle different arguments to Bundler.require return auto_required_gemspecs_from_bundler if require == 'bundler/require' - # @sg-ignore Variable type could not be inferred for gemspec - # @type [Gem::Specification, nil] - gemspec = Gem::Specification.find_by_path(require) - if gemspec.nil? - gem_name_guess = require.split('/').first + # Determine gem name based on the require path + file = "lib/#{require}.rb" + spec_with_path = Gem::Specification.find_by_path(file) + + all_gemspecs = all_gemspecs_from_bundle + + gem_names_to_try = [ + spec_with_path&.name, + require.tr('/', '-'), + require.split('/').first + ].compact.uniq + gem_names_to_try.each do |gem_name| + gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name } + return [gemspec_or_preference(gemspec)] if gemspec + begin - # this can happen when the gem is included via a local path in - # a Gemfile; Gem doesn't try to index the paths in that case. - # - # See if we can make a good guess: - potential_gemspec = Gem::Specification.find_by_name(gem_name_guess) - file = "lib/#{require}.rb" - # @sg-ignore Unresolved call to files - gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file } + gemspec = Gem::Specification.find_by_name(gem_name) + return [gemspec_or_preference(gemspec)] if gemspec rescue Gem::MissingSpecError logger.debug do - "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" + "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name}" end end + + # look ourselves just in case this is hanging out somewhere + # that find_by_path doesn't index' + gemspec = all_gemspecs.find do |spec| + spec = to_gem_specification(spec) unless spec.respond_to?(:files) + + spec&.files&.any? { |gemspec_file| file == gemspec_file } + end + return [gemspec_or_preference(gemspec)] if gemspec end - return nil if gemspec.nil? - [gemspec_or_preference(gemspec)] + nil end # @param name [String] @@ -73,10 +85,14 @@ def resolve_require require # @param out [IO, nil] output stream for logging # # @return [Gem::Specification, nil] - def find_gem name, version = nil, out = nil - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError - nil + def find_gem name, version = nil, out: $stderr + gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name && gemspec.version == version } + return gemspec if gemspec + + gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name } + return gemspec if gemspec + + resolve_gem_ignoring_local_bundle name, version, out: out end # @param gemspec [Gem::Specification] @@ -84,27 +100,233 @@ def find_gem name, version = nil, out = nil # # @return [Array] def fetch_dependencies gemspec, out: $stderr - # @param spec [Gem::Dependency] - only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| - Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" - dep = Gem.loaded_specs[spec.name] - # @todo is next line necessary? - dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) - deps.merge fetch_dependencies(dep) if deps.add?(dep) + gemspecs = all_gemspecs_from_bundle + + # @type [Hash{String => Gem::Specification}] + deps_so_far = {} + + # @param runtime_dep [Gem::Dependency] + # @param deps [Hash{String => Gem::Specification}] + gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| + next if deps[runtime_dep.name] + + Solargraph.logger.info "Adding #{runtime_dep.name} dependency for #{gemspec.name}" + dep = gemspecs.find { |dep| dep.name == runtime_dep.name } + dep ||= Gem::Specification.find_by_name(runtime_dep.name, runtime_dep.requirement) rescue Gem::MissingSpecError - Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for " \ - "#{gemspec.name} not found in RubyGems." - end.to_a + dep = resolve_gem_ignoring_local_bundle runtime_dep.name, out: out + ensure + next unless dep + + fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep } + deps[dep.name] ||= dep + end + + gem_dep_gemspecs.values.compact.uniq(&:name) + end + + # Returns all gemspecs directly depended on by this workspace's + # bundle (does not include transitive dependencies). + # + # @return [Array] + def all_gemspecs_from_bundle + return [] unless directory + + @all_gemspecs_from_bundle ||= + if in_this_bundle? + all_gemspecs_from_this_bundle + else + all_gemspecs_from_external_bundle + end + end + + # @return [Hash{Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification => Gem::Specification}] + def self.gem_specification_cache + @gem_specification_cache ||= {} end private - # True if the workspace has a root Gemfile. + # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + # @return [Gem::Specification, nil] + def to_gem_specification specish + # print time including milliseconds + self.class.gem_specification_cache[specish] ||= case specish + when Gem::Specification + @@warned_on_rubygems ||= false + if specish.respond_to?(:identifier) + specish + else + # see https://github.com/castwide/solargraph/actions/runs/17588131738/job/49961580698?pr=1006 - happened on Ruby 3.0 + unless @@warned_on_rubygems + logger.warn "Incomplete Gem::Specification encountered - recommend upgrading rubygems" + @@warned_on_rubygems = true + end + nil + end + # yay! + + when Bundler::LazySpecification + # materializing didn't work. Let's look in the local + # rubygems without bundler's help + resolve_gem_ignoring_local_bundle specish.name, + specish.version + when Bundler::StubSpecification + # turns a Bundler::StubSpecification into a + # Gem::StubSpecification into a Gem::Specification + specish = specish.stub + if specish.respond_to?(:spec) + specish.spec + else + # turn the crank again + to_gem_specification(specish) + end + else + @@warned_on_gem_type ||= false + unless @@warned_on_gem_type + logger.warn 'Unexpected type while resolving gem: ' \ + "#{specish.class}" + @@warned_on_gem_type = true + end + nil + end + end + + # @param command [String] The expression to evaluate in the external bundle + # @sg-ignore Need a JSON type + # @yield [undefined, nil] + def query_external_bundle command + Solargraph.with_clean_env do + cmd = [ + 'ruby', '-e', + "require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }" + ] + # @sg-ignore Unresolved call to capture3 on Module + o, e, s = Open3.capture3(*cmd) + if s.success? + Solargraph.logger.debug "External bundle: #{o}" + o && !o.empty? ? JSON.parse(o.split("\n").last) : nil + else + Solargraph.logger.warn e + raise BundleNotFoundError, "Failed to load gems from bundle at #{directory}" + end + end + end + + def in_this_bundle? + Bundler.definition&.lockfile&.to_s&.start_with?(directory) + end + + # @return [Array] + def all_gemspecs_from_this_bundle + # Find only the gems bundler is now using + specish_objects = Bundler.definition.locked_gems.specs + if specish_objects.first.respond_to?(:materialize_for_installation) + specish_objects = specish_objects.map(&:materialize_for_installation) + end + specish_objects.map do |specish| + if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir) + # duck type is good enough for outside uses! + specish + else + to_gem_specification(specish) + end + end.compact + end + + # @return [Array] + def auto_required_gemspecs_from_bundler + return [] unless directory + + logger.info 'Fetching gemspecs autorequired from Bundler (bundler/require)' + @auto_required_gemspecs_from_bundler ||= + if in_this_bundle? + auto_required_gemspecs_from_this_bundle + else + auto_required_gemspecs_from_external_bundle + end + end + + # @return [Array] + def auto_required_gemspecs_from_this_bundle + # Adapted from require() in lib/bundler/runtime.rb + dep_names = Bundler.definition.dependencies.select do |dep| + dep.groups.include?(:default) && dep.should_include? + end.map(&:name) + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + + # @return [Array] + def auto_required_gemspecs_from_external_bundle + @auto_required_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching auto-required gemspecs from Bundler (bundler/require)' + command = + 'Bundler.definition.dependencies' \ + '.select { |dep| dep.groups.include?(:default) && dep.should_include? }' \ + '.map(&:name)' + # @sg-ignore + # @type [Array] + dep_names = query_external_bundle command + + all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) } + end + end + + # @param gemspec [Gem::Specification] + # @return [Array] + def only_runtime_dependencies gemspec + unless gemspec.respond_to?(:dependencies) && gemspec.respond_to?(:development_dependencies) + gemspec = to_gem_specification(gemspec) + end + return [] if gemspec.nil? + + gemspec.dependencies - gemspec.development_dependencies + end + + # @todo Should this be using Gem::SpecFetcher and pull them automatically? # - # @todo Handle projects with custom Bundler/Gemfile setups (see #auto_required_gemspecs_from_bundler) + # @param name [String] + # @param version [String, nil] + # @param out [IO, nil] output stream for logging # - def gemfile? - directory && File.file?(File.join(directory, 'Gemfile')) + # @return [Gem::Specification, nil] + def resolve_gem_ignoring_local_bundle name, version = nil, out: $stderr + Gem::Specification.find_by_name(name, version) + rescue Gem::MissingSpecError + begin + Gem::Specification.find_by_name(name) + rescue Gem::MissingSpecError + stdlibmap = RbsMap::StdlibMap.new(name) + unless stdlibmap.resolved? + gem_desc = name + gem_desc += ":#{version}" if version + out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment" + end + nil # either not here or in stdlib + end + end + + # @return [Array] + def all_gemspecs_from_external_bundle + @all_gemspecs_from_external_bundle ||= + begin + logger.info 'Fetching gemspecs required from external bundle' + + command = 'specish_objects = Bundler.definition.locked_gems&.specs; ' \ + 'if specish_objects.first.respond_to?(:materialize_for_installation);' \ + 'specish_objects = specish_objects.map(&:materialize_for_installation);' \ + 'end;' \ + 'specish_objects.map { |specish| [specish.name, specish.version] }' + query_external_bundle(command).map do |name, version| + resolve_gem_ignoring_local_bundle(name, version) + end.compact + rescue Solargraph::BundleNotFoundError => e + Solargraph.logger.info e.message + Solargraph.logger.debug e.backtrace.join("\n") + [] + end end # @return [Hash{String => Gem::Specification}] @@ -113,6 +335,7 @@ def preference_map end # @param gemspec [Gem::Specification] + # # @return [Gem::Specification] def gemspec_or_preference gemspec return gemspec unless preference_map.key?(gemspec.name) @@ -122,83 +345,15 @@ def gemspec_or_preference gemspec end # @param gemspec [Gem::Specification] - # @param version [Gem::Version] + # @param version [String] # @return [Gem::Specification] def change_gemspec_version gemspec, version Gem::Specification.find_by_name(gemspec.name, "= #{version}") rescue Gem::MissingSpecError - Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead" + Solargraph.logger.info "Gem #{gemspec.name} version #{version.inspect} not found. " \ + "Using #{gemspec.version} instead" gemspec end - - # @param gemspec [Gem::Specification] - # @return [Array] - def only_runtime_dependencies gemspec - gemspec.dependencies - gemspec.development_dependencies - end - - # Return the gems which would be required by Bundler.require - # - # @see https://bundler.io/guides/groups.html - # @return [Array] - def auto_required_gemspecs_from_bundler - # @todo Handle projects with custom Bundler/Gemfile setups - return unless gemfile? - - if gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(directory) - # Find only the gems bundler is now using - Bundler.definition.locked_gems.specs.flat_map do |lazy_spec| - logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}" - [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)] - rescue Gem::MissingSpecError => e - logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with " \ - 'find_by_name, falling back to guess') - # can happen in local filesystem references - # TODO: should this be resolve_require or find_gem? - specs = resolve_require lazy_spec.name - logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - logger.info 'Fetching gemspecs required from Bundler (bundler/require)' - gemspecs_required_from_external_bundle - end - end - - # @return [Array] - def gemspecs_required_from_external_bundle - logger.info 'Fetching gemspecs required from external bundle' - return [] unless directory - - Solargraph.with_clean_env do - cmd = [ - 'ruby', '-e', - "require 'bundler'; " \ - "require 'json'; " \ - "Dir.chdir('#{directory}') { " \ - 'puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }' \ - '.to_h.to_json }' - ] - # @sg-ignore Unresolved call to capture3 on Module - o, e, s = Open3.capture3(*cmd) - if s.success? - Solargraph.logger.debug "External bundle: #{o}" - hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {} - hash.flat_map do |name, version| - Gem::Specification.find_by_name(name, version) - rescue Gem::MissingSpecError => e - logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess") - # can happen in local filesystem references - # TODO: should this be resolve_require or find_gem? - specs = resolve_require name - logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil? - next specs - end.compact - else - Solargraph.logger.warn "Failed to load gems from bundle at #{directory}: #{e}" - end - end - end end end end diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index dadc6c93c..ce7c42a62 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -110,15 +110,15 @@ class B end end - describe '#get_method_stack' do + describe '#get_method_stack', time_limit_seconds: 240 do let(:out) { StringIO.new } let(:api_map) { Solargraph::ApiMap.load_with_cache(Dir.pwd, out) } - context 'with stdlib that has vital dependencies' do + context 'with stdlib that has vital dependencies', time_limit_seconds: 240 do let(:external_requires) { ['yaml'] } let(:method_stack) { api_map.get_method_stack('YAML', 'safe_load', scope: :class) } - it 'handles the YAML gem aliased to Psych' do + it 'handles the YAML gem aliased to Psych', time_limit_seconds: 240 do expect(method_stack).not_to be_empty end end @@ -146,7 +146,8 @@ class B describe '#cache_gem' do it 'can cache gem without a bench' do api_map = Solargraph::ApiMap.new - expect { api_map.cache_gem('rake', out: StringIO.new) }.not_to raise_error + gemspec = Gem::Specification.find_by_name('backport') + expect { api_map.cache_gem(gemspec, out: StringIO.new) }.not_to raise_error end end diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index d7f40f585..24854d271 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -33,6 +33,17 @@ end end + context 'understands rspec + rspec-mocks require pattern' do + let(:requires) do + ['rspec-mocks'] + end + + it 'generates pins from gems' do + ns_pin = doc_map.pins.find { |pin| pin.path == 'RSpec::Mocks' } + expect(ns_pin).to be_a(Solargraph::Pin::Namespace) + end + end + context 'with an invalid require' do let(:requires) do ['not_a_gem'] @@ -42,17 +53,13 @@ # These are auto-required by solargraph-rspec in case the bundle # includes these gems. In our case, it doesn't! unprovided_solargraph_rspec_requires = [ + 'rspec-rails', 'actionmailer', - 'actionpack', 'activerecord', - 'activesupport', - 'airborne', - 'rspec-core', - 'rspec-expectations', - 'rspec-mocks', - 'rspec-rails', + 'shoulda-matchers', 'rspec-sidekiq', - 'shoulda-matchers' + 'airborne', + 'activesupport' ] expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) .to eq(['not_a_gem']) diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb index 6ead1060a..c1911ad42 100644 --- a/spec/workspace/gemspecs_fetch_dependencies_spec.rb +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -17,7 +17,6 @@ end it 'finds a known dependency' do - pending('https://github.com/castwide/solargraph/pull/1006') expect(deps.map(&:name)).to include('backport') end end @@ -43,8 +42,6 @@ let(:gem_name) { 'my_fake_gem' } it 'gives a useful message' do - pending('https://github.com/castwide/solargraph/pull/1006') - output = capture_both { deps.map(&:name) } expect(output).to include('Please install the gem activerecord') end @@ -55,7 +52,7 @@ let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } let(:gemspec) do - Gem::Specification.find_by_name(gem_name) + Bundler::LazySpecification.new(gem_name, nil, nil) end before do @@ -80,22 +77,15 @@ context 'with gem that exists in our bundle' do let(:gem_name) { 'undercover' } - it 'finds dependencies' do + it 'finds dependencies', time_limit_seconds: 120 do expect(deps.map(&:name)).to include('ast') end end context 'with gem does not exist in our bundle' do - let(:gemspec) do - Gem::Specification.new(fake_gem_name) - end + let(:gem_name) { 'activerecord' } - let(:gem_name) { 'undercover' } - - let(:fake_gem_name) { 'faaaaaake912' } - - it 'gives a useful message' do - pending('https://github.com/castwide/solargraph/pull/1006') + it 'gives a useful message', time_limit_seconds: 120 do dep_names = nil output = capture_both { dep_names = deps.map(&:name) } expect(output).to include('Please install the gem activerecord') diff --git a/spec/workspace/gemspecs_find_gem_spec.rb b/spec/workspace/gemspecs_find_gem_spec.rb index 76caddab3..35f5e7a15 100644 --- a/spec/workspace/gemspecs_find_gem_spec.rb +++ b/spec/workspace/gemspecs_find_gem_spec.rb @@ -62,7 +62,6 @@ end it 'complains' do - pending("implementation") gemspec expect(out.string).to include('install the gem checkoff ') @@ -92,7 +91,6 @@ end it 'complains' do - pending("implementation") gemspec expect(out.string).to include('install the gem checkoff:1.0.0') diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index aa7b8d79d..d53638600 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -180,7 +180,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/require' } it 'finds nothing' do - expect(specs).to be_nil + expect(specs).to be_empty end end end @@ -192,8 +192,6 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/require' } it 'raises' do - pending('https://github.com/castwide/solargraph/pull/1006') - expect { specs }.to raise_error(Solargraph::BundleNotFoundError) end end @@ -217,9 +215,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/gem_tasks' } - xit 'returns gems' do - pending('improved logic for require lookups') - + it 'returns gems' do expect(specs&.map(&:name)).to include('bundler') end end diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 6b00e5c33..1dfe64ae7 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -1,4 +1,14 @@ describe Solargraph::YardMap::Mapper do + before :all do # rubocop:disable RSpec/BeforeAfterAll + @api_map = Solargraph::ApiMap.load('.') + end + + def pins_with require + doc_map = Solargraph::DocMap.new([require], @api_map.workspace, out: nil) + doc_map.cache_all!(nil) + doc_map.pins + end + it 'converts nil docstrings to empty strings' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) Dir.chdir dir do @@ -14,50 +24,33 @@ it 'marks explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.cache([], rspec) - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + pin = pins_with('rspec-expectations').find { |pin| pin.path == 'RSpec::Matchers#be_truthy' } + expect(pin).not_to be_nil expect(pin.explicit?).to be(true) end it 'marks correct return type from Logger.new' do # Using logger because it's a known dependency - logger = Gem::Specification.find_by_name('logger') - Solargraph::Yardoc.cache([], logger) - registry = Solargraph::Yardoc.load!(logger) - pins = Solargraph::YardMap::Mapper.new(registry).map - pins = pins.select { |pin| pin.path == 'Logger.new' } + pins = pins_with('logger').select { |pin| pin.path == 'Logger.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks correct return type from RuboCop::Options.new' do # Using rubocop because it's a known dependency - rubocop = Gem::Specification.find_by_name('rubocop') - Solargraph::Yardoc.cache([], rubocop) - Solargraph::Yardoc.load!(rubocop) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pins = pins.select { |pin| pin.path == 'RuboCop::Options.new' } + pins = pins_with('rubocop').select { |pin| pin.path == 'RuboCop::Options.new' } expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) expect(pins.flat_map(&:signatures).map(&:return_type).uniq.map(&:to_s)).to eq(['self']) end it 'marks non-explicit methods' do # Using rspec-expectations because it's a known dependency - rspec = Gem::Specification.find_by_name('rspec-expectations') - Solargraph::Yardoc.load!(rspec) - pins = Solargraph::YardMap::Mapper.new(YARD::Registry.all).map - pin = pins.find { |pin| pin.path == 'RSpec::Matchers#expect' } + pin = pins_with('rspec-expectations').find { |pin| pin.path == 'RSpec::Matchers#expect' } expect(pin.explicit?).to be(false) end it 'adds superclass references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - pin = pins.find do |pin| + pin = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.name == 'YARD::CodeObjects::NamespaceObject' end expect(pin.closure.path).to eq('YARD::CodeObjects::ClassObject') @@ -65,21 +58,23 @@ it 'adds include references' do # Asssuming the ast gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('ast') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - inc= pins.find do |pin| + inc = pins_with('ast').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'AST::Processor::Mixin' && pin.closure.path == 'AST::Processor' end expect(inc).to be_a(Solargraph::Pin::Reference::Include) end + it 'adds corect gates' do + # Asssuming the ast gem exists because it's a known dependency + inc = pins_with('ast').find do |pin| + pin.is_a?(Solargraph::Pin::Namespace) && pin.name == 'Mixin' && pin.closure.path == 'AST::Processor' + end + expect(inc.gates).to eq(['AST::Processor::Mixin', 'AST::Processor', 'AST', '']) + end + it 'adds extend references' do # Asssuming the yard gem exists because it's a known dependency - gemspec = Gem::Specification.find_by_name('yard') - Solargraph::Yardoc.cache([], gemspec) - pins = Solargraph::YardMap::Mapper.new(Solargraph::Yardoc.load!(gemspec)).map - ext = pins.find do |pin| + ext = pins_with('yard').find do |pin| pin.is_a?(Solargraph::Pin::Reference::Extend) && pin.name == 'Enumerable' && pin.closure.path == 'YARD::Registry' end expect(ext).to be_a(Solargraph::Pin::Reference::Extend) From 2c7079c618d1fdc4eede72f35ac77d95d9119368 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 12:25:33 -0400 Subject: [PATCH 244/524] Handle LazySpecification issue --- 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 ac6b4cd79..009ca8941 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -23,7 +23,7 @@ class DocMap # @return [Array] def uncached_gemspecs uncached_yard_gemspecs.concat(uncached_rbs_collection_gemspecs) - .sort + .sort_by { |gemspec| "#{gemspec.name}:#{gemspec.version}" } .uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" } end From 5a982ad8cd644542409268c7f7c91e1c3c011efd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 12:26:15 -0400 Subject: [PATCH 245/524] Drop @sg-ignore --- lib/solargraph/workspace/gemspecs.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 55dac6c6a..d1db9dc20 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -201,7 +201,6 @@ def query_external_bundle command 'ruby', '-e', "require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }" ] - # @sg-ignore Unresolved call to capture3 on Module o, e, s = Open3.capture3(*cmd) if s.success? Solargraph.logger.debug "External bundle: #{o}" From 57af0ebffe1f226c5ea25f44cbc7ac15859c8102 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 12:29:38 -0400 Subject: [PATCH 246/524] Handle LazySpecification issue --- 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 009ca8941..57706d212 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -105,7 +105,7 @@ def cache_rbs_collection_pins(gemspec, out) # @param out [IO, nil] output stream for logging # @return [void] def cache(gemspec, rebuild: false, out: nil) - build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild + build_yard = uncached_yard_gemspecs.map { |gs| "#{gs.name}:#{gs.version}" }.include?("#{gemspec.name}:#{gemspec.version}") || rebuild build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild if build_yard || build_rbs_collection type = [] From e1db9a858f5eb3cb330f1c612cae9348351acf7a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 12:38:27 -0400 Subject: [PATCH 247/524] Handle LazySpecification issue --- 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 57706d212..5c4db1cdf 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -106,7 +106,7 @@ def cache_rbs_collection_pins(gemspec, out) # @return [void] def cache(gemspec, rebuild: false, out: nil) build_yard = uncached_yard_gemspecs.map { |gs| "#{gs.name}:#{gs.version}" }.include?("#{gemspec.name}:#{gemspec.version}") || rebuild - build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild + build_rbs_collection = uncached_rbs_collection_gemspecs.map { |gs| "#{gs.name}:#{gs.version}" }.include?("#{gemspec.name}:#{gemspec.version}") || rebuild if build_yard || build_rbs_collection type = [] type << 'YARD' if build_yard From 719f44130ab9b07f06fa724eabd588df1a10b9df Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 13:11:20 -0400 Subject: [PATCH 248/524] Adjust specs --- spec/doc_map_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 24854d271..40e53ee1c 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -59,7 +59,8 @@ 'shoulda-matchers', 'rspec-sidekiq', 'airborne', - 'activesupport' + 'activesupport', + 'actionpack' ] expect(doc_map.unresolved_requires - unprovided_solargraph_rspec_requires) .to eq(['not_a_gem']) @@ -125,7 +126,9 @@ let(:requires) { ['rspec'] } it 'collects dependencies' do - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') + # we include doc_map.requires as solargraph-rspec will bring it + # in directly and we exclude it from dependencies + expect(doc_map.dependencies.map(&:name) + doc_map.requires).to include('rspec-core') end end From 81ae528a633f654b67414eaea87b56ab8b796203 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 17:49:55 -0400 Subject: [PATCH 249/524] CLI 'solargraph gems' improvements * Consolidate shell.rb's gem resolution behind the Workspace class so it can take advantage of improvements in other PRs (e.g., pulling info from the bundle directly to resolve gemspecs) * Add documentation on 'solargraph gems' * Allow request to cache core pins from 'solargraph cache' and 'solargraph gems' * Ensure stdlib is cached in 'solargraph gems' --- .rubocop_todo.yml | 1 - lib/solargraph/api_map.rb | 18 ++++------ lib/solargraph/doc_map.rb | 1 - lib/solargraph/shell.rb | 67 ++++++++++++++++++++++++------------- lib/solargraph/workspace.rb | 28 ++++++++++++++-- spec/api_map_method_spec.rb | 32 ++++++++++++++++++ spec/shell_spec.rb | 37 +++++++++++++++++--- spec/workspace_spec.rb | 37 ++++++++++++++++++++ 8 files changed, 177 insertions(+), 44 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 425e66c9a..e86360523 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -80,7 +80,6 @@ Layout/ElseAlignment: # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - - 'lib/solargraph/doc_map.rb' - 'lib/solargraph/language_server/message/initialize.rb' - 'lib/solargraph/pin/delegated_method.rb' diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 695de0230..8a45a3ebd 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -123,8 +123,10 @@ def catalog bench [self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires] end - # @return [DocMap, nil] - attr_reader :doc_map + # @return [DocMap] + def doc_map + @doc_map ||= DocMap.new([], [], Workspace.new('.')) + end # @return [::Array] def uncached_gemspecs @@ -189,15 +191,7 @@ def self.load directory # @param out [IO, nil] # @return [void] def cache_all_for_doc_map! out - @doc_map.cache_doc_map_gems!(out) - end - - # @param gemspec [Gem::Specification] - # @param rebuild [Boolean] - # @param out [IO, nil] - # @return [void] - def cache_gem(gemspec, rebuild: false, out: nil) - @doc_map.cache(gemspec, rebuild: rebuild, out: out) + doc_map.cache_doc_map_gems!(out) end class << self @@ -676,7 +670,7 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected] # @return [Workspace, nil] def workspace - @doc_map&.workspace + doc_map.workspace end # @param fq_reference_tag [String] A fully qualified whose method should be pulled in diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 9f119c94c..0691110f5 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -250,7 +250,6 @@ def only_runtime_dependencies gemspec gemspec.dependencies - gemspec.development_dependencies end - def inspect self.class.inspect end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 735e6c290..240fac654 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -104,9 +104,8 @@ def clear # @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) - api_map.cache_gem(spec, rebuild: options[:rebuild], out: $stdout) + gems(gem + (version ? "=#{version}" : '')) + # ' end desc 'uncache GEM [...GEM]', "Delete specific cached gem documentation" @@ -132,28 +131,60 @@ def uncache *gems next end - spec = Gem::Specification.find_by_name(gem) + spec = workspace.find_gem(gem) workspace.uncache_gem(spec, out: $stdout) end end - desc 'gems [GEM[=VERSION]]', 'Cache documentation for installed gems' + desc 'gems [GEM[=VERSION]...] [STDLIB...] [core]', 'Cache documentation for + installed libraries' + long_desc %( This command will cache the + generated type documentation for the specified libraries. While + Solargraph will generate this on the fly when needed, it takes + time. This command will generate it in advance, which can be + useful for CI scenarios. + + With no arguments, it will cache all libraries in the current + workspace. If a gem or standard library name is specified, it + will cache that library's type documentation. + + An equals sign after a gem will allow a specific gem version + to be cached. + + The 'core' argument can be used to cache the type + documentation for the core Ruby libraries. + + If the library is already cached, it will be rebuilt if the + --rebuild option is set. + + Cached documentation is stored in #{PinCache.base_dir}, which + can be stored between CI runs. + ) option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false # @param names [Array] # @return [void] def gems *names - api_map = ApiMap.load('.') + # print time with ms + workspace = Solargraph::Workspace.new('.') + if names.empty? - Gem::Specification.to_a.each { |spec| do_cache spec, api_map } - STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems." + workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild]) else + $stderr.puts("Caching these gems: #{names}") names.each do |name| - spec = Gem::Specification.find_by_name(*name.split('=')) - do_cache spec, api_map - rescue Gem::MissingSpecError - warn "Gem '#{name}' not found" + if name == 'core' + PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild] + next + end + + gemspec = workspace.find_gem(*name.split('=')) + if gemspec.nil? + warn "Gem '#{name}' not found" + else + workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) + end end - STDERR.puts "Documentation cached for #{names.count} gems." + $stderr.puts "Documentation cached for #{names.count} gems." end end @@ -194,7 +225,6 @@ def typecheck *files filecount += 1 probcount += problems.length end - # " } puts "Typecheck finished in #{time.real} seconds." puts "#{probcount} problem#{probcount != 1 ? 's' : ''} found#{files.length != 1 ? " in #{filecount} of #{files.length} files" : ''}." @@ -261,14 +291,5 @@ def pin_description pin desc += " (#{pin.location.filename} #{pin.location.range.start.line})" if pin.location desc end - - # @param gemspec [Gem::Specification] - # @param api_map [ApiMap] - # @return [void] - 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) - end end end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 2697e90e0..5e0f07750 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -22,7 +22,8 @@ class Workspace attr_reader :gemnames alias source_gems gemnames - # @param directory [String] TODO: Document and test '' and '*' semantics + # @todo Remove '' and '*' special cases + # @param directory [String] # @param config [Config, nil] # @param server [Hash] def initialize directory = '', config = nil, server = {} @@ -66,7 +67,7 @@ def stdlib_dependencies stdlib_name def global_environ # empty docmap, since the result needs to work in any possible # context here - @global_environ ||= Convention.for_global(DocMap.new([], [], self)) + @global_environ ||= Convention.for_global(DocMap.new([], [], self, out: nil)) end # @param gemspec [Gem::Specification, Bundler::LazySpecification] @@ -195,6 +196,29 @@ def find_gem name, version = nil nil end + # @param out [IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # @return [void] + def cache_all_for_workspace! out, rebuild: false + PinCache.cache_core(out: out) unless PinCache.core? + + # @type [Array] + gem_specs = Gem::Specification.to_a + # try any possible standard libraries, but be quiet about it + stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact + specs = (gem_specs + stdlib_specs) + specs.each do |spec| + pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec) + end + out&.puts "Documentation cached for all #{specs.length} gems." + + # do this after so that we prefer stdlib requires from gems, + # which are likely to be newer and have more pins + pin_cache.cache_all_stdlibs(out: out) + + out&.puts "Documentation cached for core, standard library and gems." + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index 9d4e4f553..b4202e557 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -133,6 +133,38 @@ class B end end + describe '#cache_all_for_doc_map!' do + it 'can cache gems without a bench' do + api_map = Solargraph::ApiMap.new + doc_map = instance_double(Solargraph::DocMap, cache_doc_map_gems!: true) + allow(Solargraph::DocMap).to receive(:new).and_return(doc_map) + api_map.cache_all_for_doc_map!($stderr) + expect(doc_map).to have_received(:cache_doc_map_gems!).with($stderr) + end + end + + describe '#cache_gem' do + it 'can cache gem without a bench' do + api_map = Solargraph::ApiMap.new + gemspec = Gem::Specification.find_by_name('backport') + expect { api_map.cache_gem(gemspec, out: StringIO.new) }.not_to raise_error + end + end + + describe '#workspace' do + it 'can get a default workspace without a bench' do + api_map = Solargraph::ApiMap.new + expect(api_map.workspace).not_to be_nil + end + end + + describe '#uncached_gemspecs' do + it 'can get uncached gemspecs workspace without a bench' do + api_map = Solargraph::ApiMap.new + expect(api_map.uncached_gemspecs).not_to be_nil + end + end + describe '#get_methods' do it 'recognizes mixin references from context' do source = Solargraph::Source.load_string(%( diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index 6e2a83074..f5eeb5734 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -10,12 +10,14 @@ before do File.open(File.join(temp_dir, 'Gemfile'), 'w') do |file| file.puts "source 'https://rubygems.org'" - file.puts "gem 'solargraph', path: #{File.expand_path('..', __dir__)}" + file.puts "gem 'solargraph', path: '#{File.expand_path('..', __dir__)}'" end output, status = Open3.capture2e("bundle install", chdir: temp_dir) raise "Failure installing bundle: #{output}" unless status.success? end + # @type cmd [Array] + # @return [String] def bundle_exec(*cmd) # run the command in the temporary directory with bundle exec output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}", chdir: temp_dir) @@ -111,8 +113,6 @@ def bundle_exec(*cmd) end it 'caches core without erroring out' do - pending('https://github.com/castwide/solargraph/pull/1061') - capture_both do shell.uncache('core') end @@ -128,6 +128,35 @@ def bundle_exec(*cmd) expect(output).to include("Gem 'solargraph123' not found") end end + + context 'with mocked Workspace' do + let(:workspace) { instance_double(Solargraph::Workspace) } + let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } + + before do + allow(Solargraph::Workspace).to receive(:new).and_return(workspace) + end + + it 'caches all without erroring out' do + allow(workspace).to receive(:cache_all_for_workspace!) + + _output = capture_both { shell.gems } + + expect(workspace).to have_received(:cache_all_for_workspace!) + end + + it 'caches single gem without erroring out' do + allow(workspace).to receive(:find_gem).with('backport').and_return(gemspec) + allow(workspace).to receive(:cache_gem) + + capture_both do + shell.options = { rebuild: false } + shell.gems('backport') + end + + expect(workspace).to have_received(:cache_gem).with(gemspec, out: an_instance_of(StringIO), rebuild: false) + end + end end describe 'cache' do @@ -139,8 +168,6 @@ def bundle_exec(*cmd) subject(:call) { shell.cache('nonexistentgem8675309') } it 'gives a good error message' do - pending('https://github.com/castwide/solargraph/pull/1061') - # capture stderr output expect { call }.to output(/not found/).to_stderr end diff --git a/spec/workspace_spec.rb b/spec/workspace_spec.rb index db8141249..8b5a67aa8 100644 --- a/spec/workspace_spec.rb +++ b/spec/workspace_spec.rb @@ -132,4 +132,41 @@ Solargraph::Workspace.new('./path', config) }.not_to raise_error end + + describe '#cache_all_for_workspace!' do + let(:pin_cache) { instance_double(Solargraph::PinCache) } + + before do + allow(Solargraph::PinCache).to receive(:cache_core) + allow(Solargraph::PinCache).to receive(:possible_stdlibs) + allow(Solargraph::PinCache).to receive(:new).and_return(pin_cache) + allow(pin_cache).to receive_messages(cache_gem: nil, possible_stdlibs: []) + allow(Solargraph::PinCache).to receive(:cache_all_stdlibs) + end + + it 'caches core pins' do + allow(Solargraph::PinCache).to receive_messages(core?: false) + allow(pin_cache).to receive_messages(cached?: true, + cache_all_stdlibs: nil) + + workspace.cache_all_for_workspace!(nil, rebuild: false) + + expect(Solargraph::PinCache).to have_received(:cache_core).with(out: nil) + end + + it 'caches gems' do + gemspec = instance_double(Gem::Specification, name: 'test_gem', version: '1.0.0') + allow(Gem::Specification).to receive(:to_a).and_return([gemspec]) + allow(pin_cache).to receive(:cached?).and_return(false) + allow(pin_cache).to receive(:cache_all_stdlibs).with(out: nil) + + allow(Solargraph::PinCache).to receive_messages(core?: true, + possible_stdlibs: []) + + workspace.cache_all_for_workspace!(nil, rebuild: false) + + expect(pin_cache).to have_received(:cache_gem).with(gemspec: gemspec, out: nil, + rebuild: false) + end + end end From 915c5aab23a3f048d1ae2a5f021be578f2121f9d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 18:03:37 -0400 Subject: [PATCH 250/524] Allow PR to build --- .github/workflows/linting.yml | 2 +- .github/workflows/plugins.yml | 2 +- .github/workflows/rspec.yml | 2 +- .github/workflows/typecheck.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b4ef26bfe..6262dd494 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -8,7 +8,7 @@ name: Linting on: workflow_dispatch: {} pull_request: - branches: [ master ] + branches: ['*'] push: branches: - 'main' diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..871252fcc 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -9,7 +9,7 @@ on: push: branches: [master] pull_request: - branches: [master] + branches: ['*'] permissions: contents: read diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 6fe05a9b9..12399f50b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: ['*'] permissions: contents: read diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 0ae8a3d8a..9f0020065 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: ['*'] permissions: contents: read From 9e29680b0e7d67f4c6165db6d028d3ea87f806c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 6 Oct 2025 18:11:47 -0400 Subject: [PATCH 251/524] Drop spec --- spec/api_map_method_spec.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index b4202e557..0ee0238cf 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -143,14 +143,6 @@ class B end end - describe '#cache_gem' do - it 'can cache gem without a bench' do - api_map = Solargraph::ApiMap.new - gemspec = Gem::Specification.find_by_name('backport') - expect { api_map.cache_gem(gemspec, out: StringIO.new) }.not_to raise_error - end - end - describe '#workspace' do it 'can get a default workspace without a bench' do api_map = Solargraph::ApiMap.new From 7b4982392773415b9e25bafddebeefedad81e61e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 07:29:09 -0400 Subject: [PATCH 252/524] Add space for better future merge --- lib/solargraph/workspace.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 5e0f07750..36e85efe8 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -196,14 +196,21 @@ def find_gem name, version = nil nil end + # @todo make this actually work against bundle instead of pulling + # all installed gemspecs - + # https://github.com/apiology/solargraph/pull/10 + # @return [Array] + def all_gemspecs_from_bundle + Gem::Specification.to_a + end + # @param out [IO, nil] output stream for logging # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # @return [void] def cache_all_for_workspace! out, rebuild: false PinCache.cache_core(out: out) unless PinCache.core? - # @type [Array] - gem_specs = Gem::Specification.to_a + gem_specs = all_gemspecs_from_bundle # try any possible standard libraries, but be quiet about it stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact specs = (gem_specs + stdlib_specs) From b2a562afd0d282a8eb9282783b994d16b6186fa0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 07:41:51 -0400 Subject: [PATCH 253/524] Add space for better future merge --- lib/solargraph/workspace.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index e2d3d7495..dfe27dee8 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -162,6 +162,11 @@ def rbs_collection_config_path end end + # @return [Array] + def all_gemspecs_from_bundle + gemspecs.all_gemspecs_from_bundle + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] From 6e8339946db3b0a21592ece09bcd9f8e33777186 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 07:50:40 -0400 Subject: [PATCH 254/524] Revert --- lib/solargraph/workspace.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index dfe27dee8..e2d3d7495 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -162,11 +162,6 @@ def rbs_collection_config_path end end - # @return [Array] - def all_gemspecs_from_bundle - gemspecs.all_gemspecs_from_bundle - end - # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] From 71ac7b56c7fabdde2f5eb7bbdb7a1f54a3edf4fe Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 07:52:07 -0400 Subject: [PATCH 255/524] Add space for better future merge --- lib/solargraph/workspace.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 69ad12bae..c44bfb36d 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -170,6 +170,11 @@ def find_gem name, version = nil gemspecs.find_gem(name, version) end + # @return [Array] + def all_gemspecs_from_bundle + gemspecs.all_gemspecs_from_bundle + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] From 9d7ed97bf0f80b6191afdc1d698c4c79934d53ae Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 08:08:35 -0400 Subject: [PATCH 256/524] Add space for better future merge --- lib/solargraph/workspace.rb | 7 +++++++ lib/solargraph/workspace/gemspecs.rb | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index c44bfb36d..50c9f4519 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -47,6 +47,13 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + gemspecs.stdlib_dependencies(stdlib_name) + end + # @param out [IO, nil] output stream for logging # @param gemspec [Gem::Specification] # @return [Array] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index d1db9dc20..c6ed33d58 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -80,6 +80,15 @@ def resolve_require require nil end + # @todo space for future expansion from other merges + # + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + [] + end + # @param name [String] # @param version [String, nil] # @param out [IO, nil] output stream for logging From 66870f09e727bce634ae0a72a88d6eebf7370b2a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 08:25:43 -0400 Subject: [PATCH 257/524] Add more space for future merges --- lib/solargraph/workspace/gemspecs.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index c6ed33d58..46c92ed4e 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -83,9 +83,10 @@ def resolve_require require # @todo space for future expansion from other merges # # @param stdlib_name [String] + # @param stdlib_version [String, nil] # # @return [Array] - def stdlib_dependencies stdlib_name + def stdlib_dependencies stdlib_name, stdlib_version = nil [] end @@ -119,6 +120,7 @@ def fetch_dependencies gemspec, out: $stderr gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| next if deps[runtime_dep.name] + # TODO Why doesn't this just delegate to find_gem? Add comment or change Solargraph.logger.info "Adding #{runtime_dep.name} dependency for #{gemspec.name}" dep = gemspecs.find { |dep| dep.name == runtime_dep.name } dep ||= Gem::Specification.find_by_name(runtime_dep.name, runtime_dep.requirement) @@ -131,7 +133,10 @@ def fetch_dependencies gemspec, out: $stderr deps[dep.name] ||= dep end - gem_dep_gemspecs.values.compact.uniq(&:name) + (gem_dep_gemspecs.values.compact + + # try stdlib as well + stdlib_dependencies(gemspec.name, gemspec.version)). + uniq(&:name) end # Returns all gemspecs directly depended on by this workspace's From 7365c35b986b1c07d4a59ab5e514a509092d212e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 10:37:12 -0400 Subject: [PATCH 258/524] Simplify code with find_gem, satisfy rubocop --- lib/solargraph/workspace/gemspecs.rb | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 46c92ed4e..9fe2c60fb 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -118,25 +118,18 @@ def fetch_dependencies gemspec, out: $stderr # @param runtime_dep [Gem::Dependency] # @param deps [Hash{String => Gem::Specification}] gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| - next if deps[runtime_dep.name] - - # TODO Why doesn't this just delegate to find_gem? Add comment or change - Solargraph.logger.info "Adding #{runtime_dep.name} dependency for #{gemspec.name}" - dep = gemspecs.find { |dep| dep.name == runtime_dep.name } - dep ||= Gem::Specification.find_by_name(runtime_dep.name, runtime_dep.requirement) - rescue Gem::MissingSpecError - dep = resolve_gem_ignoring_local_bundle runtime_dep.name, out: out - ensure + dep = find_gem(runtime_dep.name, runtime_dep.requirement) next unless dep fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep } + deps[dep.name] ||= dep end (gem_dep_gemspecs.values.compact + # try stdlib as well - stdlib_dependencies(gemspec.name, gemspec.version)). - uniq(&:name) + stdlib_dependencies(gemspec.name, gemspec.version)) + .uniq(&:name) end # Returns all gemspecs directly depended on by this workspace's @@ -301,12 +294,12 @@ def only_runtime_dependencies gemspec # @todo Should this be using Gem::SpecFetcher and pull them automatically? # # @param name [String] - # @param version [String, nil] + # @param version_or_requirement [String, nil] # @param out [IO, nil] output stream for logging # # @return [Gem::Specification, nil] - def resolve_gem_ignoring_local_bundle name, version = nil, out: $stderr - Gem::Specification.find_by_name(name, version) + def resolve_gem_ignoring_local_bundle name, version_or_requirement = nil, out: $stderr + Gem::Specification.find_by_name(name, version_or_requirement) rescue Gem::MissingSpecError begin Gem::Specification.find_by_name(name) @@ -314,7 +307,7 @@ def resolve_gem_ignoring_local_bundle name, version = nil, out: $stderr stdlibmap = RbsMap::StdlibMap.new(name) unless stdlibmap.resolved? gem_desc = name - gem_desc += ":#{version}" if version + gem_desc += ":#{version_or_requirement}" if version_or_requirement out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment" end nil # either not here or in stdlib From ba1400cb3c9a0a1e45a05899e12fac62b62c6a5a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 11:01:48 -0400 Subject: [PATCH 259/524] Don't rely on gemspec being hashable --- lib/solargraph/doc_map.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5c4db1cdf..95ee9daf3 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -288,11 +288,12 @@ def deserialize_stdlib_rbs_map path # @param rbs_version_cache_key [String] # @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]) + key = "#{gemspec.name}:#{gemspec.version}" + return if rbs_collection_pins_in_memory.key?([key, 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 + rbs_collection_pins_in_memory[[key, rbs_version_cache_key]] = cached cached else logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}" From fa96091426d4e3e1badd4d48314c58ea737df41c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 13:11:00 -0400 Subject: [PATCH 260/524] Remove unused methods --- lib/solargraph/rbs_map/stdlib_map.rb | 16 ++++++++++++++++ lib/solargraph/workspace.rb | 7 ------- lib/solargraph/workspace/gemspecs.rb | 19 +++++-------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index b6804157f..9308e8041 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -33,6 +33,22 @@ def initialize library end end + # @return [RBS::Collection::Sources::Stdlib] + def self.source + @source ||= RBS::Collection::Sources::Stdlib.instance + end + + # @param name [String] + # @param version [String, nil] + # @return [Array String}>, nil] + def self.stdlib_dependencies name, version = nil + if source.has?(name, version) + source.dependencies_of(name, version) + else + [] + end + end + # @param library [String] # @return [StdlibMap] def self.load library diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 50c9f4519..c44bfb36d 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -47,13 +47,6 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end - # @param stdlib_name [String] - # - # @return [Array] - def stdlib_dependencies stdlib_name - gemspecs.stdlib_dependencies(stdlib_name) - end - # @param out [IO, nil] output stream for logging # @param gemspec [Gem::Specification] # @return [Array] diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 9fe2c60fb..4482f63e7 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -80,16 +80,6 @@ def resolve_require require nil end - # @todo space for future expansion from other merges - # - # @param stdlib_name [String] - # @param stdlib_version [String, nil] - # - # @return [Array] - def stdlib_dependencies stdlib_name, stdlib_version = nil - [] - end - # @param name [String] # @param version [String, nil] # @param out [IO, nil] output stream for logging @@ -126,10 +116,11 @@ def fetch_dependencies gemspec, out: $stderr deps[dep.name] ||= dep end - (gem_dep_gemspecs.values.compact + - # try stdlib as well - stdlib_dependencies(gemspec.name, gemspec.version)) - .uniq(&:name) + # RBS tracks implicit dependencies, like how the YAML standard + # library implies pulling in the psych library. + stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || [] + stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact + (gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name) end # Returns all gemspecs directly depended on by this workspace's From 7d8b5a36aecb8103c7cb353f132fad99dd49b7ff Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 13:53:08 -0400 Subject: [PATCH 261/524] Add back stdlib_dependencies() for use in future merge --- lib/solargraph/workspace.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index c44bfb36d..50c9f4519 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -47,6 +47,13 @@ def config @config ||= Solargraph::Workspace::Config.new(directory) end + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + gemspecs.stdlib_dependencies(stdlib_name) + end + # @param out [IO, nil] output stream for logging # @param gemspec [Gem::Specification] # @return [Array] From 149962da4c4610ee976b6d7c95cf0dcbbd98d621 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 13:53:26 -0400 Subject: [PATCH 262/524] Add back stdlib_dependencies() for use in future merge --- lib/solargraph/workspace/gemspecs.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 4482f63e7..c34120451 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -80,6 +80,14 @@ def resolve_require require nil end + # @param stdlib_name [String] + # + # @return [Array] + def stdlib_dependencies stdlib_name + deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || [] + deps.map { |dep| dep['name'] }.compact + end + # @param name [String] # @param version [String, nil] # @param out [IO, nil] output stream for logging From d9fa32f6bd89dceefb1eafc423a3dafaefa97eaf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 15:54:51 -0400 Subject: [PATCH 263/524] Fix a Gem::Specification class limitation found on another branch --- lib/solargraph/doc_map.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 9f119c94c..79ba2c5d8 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -92,14 +92,16 @@ def unresolved_requires @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys end - # @return [Set] + # @return [Array] # @param out [IO] def dependencies out: $stderr @dependencies ||= begin - all_deps = gemspecs.flat_map { |spec| fetch_dependencies(spec, out: out) } + all_deps = gemspecs. + flat_map { |spec| fetch_dependencies(spec, out: out) }. + uniq(&:name) existing_gems = gemspecs.map(&:name) - all_deps.reject { |gemspec| existing_gems.include? gemspec.name }.to_set + all_deps.reject { |gemspec| existing_gems.include? gemspec.name } end end From f4f1620db6319034202fc2c72afce5f9d9db2f0d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 16:00:34 -0400 Subject: [PATCH 264/524] Fix RuboCop issues --- lib/solargraph/doc_map.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 79ba2c5d8..cc3a4aed6 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -97,9 +97,9 @@ def unresolved_requires def dependencies out: $stderr @dependencies ||= begin - all_deps = gemspecs. - flat_map { |spec| fetch_dependencies(spec, out: out) }. - uniq(&:name) + all_deps = gemspecs + .flat_map { |spec| fetch_dependencies(spec, out: out) } + .uniq(&:name) existing_gems = gemspecs.map(&:name) all_deps.reject { |gemspec| existing_gems.include? gemspec.name } end From bf950718d1180b6aabe12a35b90c82f88fc34a6a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 8 Oct 2025 19:12:11 -0400 Subject: [PATCH 265/524] Restore actions config --- .github/workflows/linting.yml | 2 +- .github/workflows/plugins.yml | 2 +- .github/workflows/rspec.yml | 2 +- .github/workflows/typecheck.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6262dd494..b4ef26bfe 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -8,7 +8,7 @@ name: Linting on: workflow_dispatch: {} pull_request: - branches: ['*'] + branches: [ master ] push: branches: - 'main' diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 871252fcc..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -9,7 +9,7 @@ on: push: branches: [master] pull_request: - branches: ['*'] + branches: [master] permissions: contents: read diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 12399f50b..6fe05a9b9 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: ['*'] + branches: [ master ] permissions: contents: read diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 9f0020065..0ae8a3d8a 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -11,7 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: ['*'] + branches: [ master ] permissions: contents: read From 5ea669e6b34d9f54ec3b4a275388997a83ec47c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:17:23 -0400 Subject: [PATCH 266/524] Remove temporary changes --- .github/workflows/linting.yml | 5 ++--- .github/workflows/plugins.yml | 4 +--- .github/workflows/rspec.yml | 4 +--- .github/workflows/typecheck.yml | 3 +-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index aaefe0c16..ae885b4db 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -8,11 +8,10 @@ name: Linting on: workflow_dispatch: {} pull_request: - branches: - - '*' + branches: [ master ] push: branches: - - 'main' + - 'master' tags: - 'v*' diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 01169ce7f..b862c8343 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -8,9 +8,7 @@ name: Plugin on: push: branches: [master] - pull_request: - branches: - - '*' + pull_request: [master] permissions: contents: read diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 8b7e87f79..fc4eee47a 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -10,9 +10,7 @@ name: RSpec on: push: branches: [ master ] - pull_request: - branches: - - '*' + pull_request: [ master ] permissions: contents: read diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 26eb75a17..0ae8a3d8a 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -11,8 +11,7 @@ on: push: branches: [ master ] pull_request: - branches: - - '*' + branches: [ master ] permissions: contents: read From 642163756e9a5fe3f34857b513fc8f299ecd3e4c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:20:14 -0400 Subject: [PATCH 267/524] Remove temporary changes --- Rakefile | 2 +- spec/api_map_method_spec.rb | 6 +++--- spec/workspace/gemspecs_fetch_dependencies_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Rakefile b/Rakefile index 793831b14..d731fc786 100755 --- a/Rakefile +++ b/Rakefile @@ -54,7 +54,7 @@ def undercover cmd = 'bundle exec undercover ' \ '--simplecov coverage/combined/coverage.json ' \ '--exclude-files "Rakefile,spec/*,spec/**/*,lib/solargraph/version.rb" ' \ - '--compare origin/extract_gemspecs_logic_from_doc_map' + '--compare origin/master' output, status = Bundler.with_unbundled_env do Open3.capture2e(cmd) end diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index ce7c42a62..610ab5484 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -110,15 +110,15 @@ class B end end - describe '#get_method_stack', time_limit_seconds: 240 do + describe '#get_method_stack' do let(:out) { StringIO.new } let(:api_map) { Solargraph::ApiMap.load_with_cache(Dir.pwd, out) } - context 'with stdlib that has vital dependencies', time_limit_seconds: 240 do + context 'with stdlib that has vital dependencies' do let(:external_requires) { ['yaml'] } let(:method_stack) { api_map.get_method_stack('YAML', 'safe_load', scope: :class) } - it 'handles the YAML gem aliased to Psych', time_limit_seconds: 240 do + it 'handles the YAML gem aliased to Psych' do expect(method_stack).not_to be_empty end end diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb index c1911ad42..56504e7dd 100644 --- a/spec/workspace/gemspecs_fetch_dependencies_spec.rb +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -77,7 +77,7 @@ context 'with gem that exists in our bundle' do let(:gem_name) { 'undercover' } - it 'finds dependencies', time_limit_seconds: 120 do + it 'finds dependencies' do expect(deps.map(&:name)).to include('ast') end end @@ -85,7 +85,7 @@ context 'with gem does not exist in our bundle' do let(:gem_name) { 'activerecord' } - it 'gives a useful message', time_limit_seconds: 120 do + it 'gives a useful message' do dep_names = nil output = capture_both { dep_names = deps.map(&:name) } expect(output).to include('Please install the gem activerecord') From d45652b82abdead4dd9b954e10f4e7fcec605e67 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:21:11 -0400 Subject: [PATCH 268/524] Remove temporary changes --- .github/workflows/plugins.yml | 4 ++-- .github/workflows/rspec.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b862c8343..142b3d5b0 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -8,8 +8,8 @@ name: Plugin on: push: branches: [master] - pull_request: [master] - + pull_request: + branches: [master] permissions: contents: read diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index fc4eee47a..bf812da99 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -10,7 +10,8 @@ name: RSpec on: push: branches: [ master ] - pull_request: [ master ] + pull_request: + branches: [ master ] permissions: contents: read From f3ca90cf3ab7c5991905646767e6714f06f8f5b8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:21:40 -0400 Subject: [PATCH 269/524] Remove temporary changes --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index bf812da99..cc4efda4b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -86,4 +86,4 @@ jobs: run: bundle exec rake spec - name: Check PR coverage run: bundle exec rake undercover - # continue-on-error: true TODO: Restore before merging + continue-on-error: true From d6aacef9439fb15dd25bae49ff824993860eef3a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:29:06 -0400 Subject: [PATCH 270/524] Remove temporary changes --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 142b3d5b0..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -10,6 +10,7 @@ on: branches: [master] pull_request: branches: [master] + permissions: contents: read From 1a2a546f0b47549089fec4fc5d3190d2819ed150 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 08:58:00 -0400 Subject: [PATCH 271/524] Add future specs --- spec/parser/flow_sensitive_typing_spec.rb | 709 +++++++++++++++++++++- 1 file changed, 707 insertions(+), 2 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index bf747fc76..30b813d85 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -3,7 +3,7 @@ # @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 + it 'uses is_a? in a simple if() to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -72,7 +72,7 @@ def verify_repro(repr) 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 + it 'uses is_a? in a simple unless statement to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -212,6 +212,42 @@ class Repro < ReproBase; end expect(clip.infer.to_s).to eq('Float') end + it 'uses varname in a "break unless" statement in a while to refine types' do + pending 'lib/solargraph/pin/breakable.rb' + + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase, nil] + value = bar + while !is_done() + break unless value + 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('ReproBase') + end + + it 'uses varname in a "break if" statement in a while to refine types' do + pending 'lib/solargraph/pin/breakable.rb' + + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase, nil] + value = bar + while !is_done() + break if value.nil? + 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('ReproBase') + end + it 'understands compatible reassignments' do source = Solargraph::Source.load_string(%( class Foo @@ -253,4 +289,673 @@ def baz; end clip = api_map.clip_at('test.rb', [3, 6]) expect { clip.infer.to_s }.not_to raise_error end + + it 'uses nil? in a simple if() to refine nilness' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, 10, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + # the 10 here is arguably a bug + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? first' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? second' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and || in a simple if() - nil? first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and || in a simple if() - nil? second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses varname and || in a simple if() - varname first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending('supporting else after || on varname') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname and || in a simple if() - varname second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending('supporting else after || on varname') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses .nil? and or in an unless' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [String, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr) + repr unless repr.nil? || repr.downcase + repr + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 33]) + expect(clip.infer.rooted_tags).to eq('::String') + + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::String, nil') + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.rooted_tags).to eq('::String, nil') + end + + it 'uses varname and && in a simple if() - varname first' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses varname and && in a simple if() - varname second' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + pending('supporting else after && on varname') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses variable in a simple if() to refine types' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, 10, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses variable in a simple if() to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + def verify_repro(repr = nil) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [3, 8]) + expect(clip.infer.rooted_tags).to eq('10, nil') + + pending('deferring nil removal in flow senstive typing') + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + pending('supporting else in flow senstiive typing') + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses .nil? in a return if() in an if to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + if rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf + # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html + it 'uses .nil? in a return if() in a method to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + return if baz.nil? + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a block to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param arr [Array] + # @return [void] + def bar(arr, baz: nil) + baz + arr.each do |item| + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending('better scoping of return if in blocks') + + clip = api_map.clip_at('test.rb', [9, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an unless to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + unless rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in unless') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a while to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + while rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an until to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + until rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a switch/case/else to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + case rand + when 0..0.5 + return if baz.nil? + baz + else + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in case/when') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [12, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a ternary operator to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + rand > 0.5 ? (return if baz.nil?; baz) : baz + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [6, 44]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in ternary operator') + + clip = api_map.clip_at('test.rb', [6, 51]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a begin/end to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a ||= to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + baz ||= begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a try / rescue / ensure to refine types using nil checks' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + rescue StandardError + baz + ensure + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in begin/rescue/ensure') + + clip = api_map.clip_at('test.rb', [12, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [14, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'provides a useful pin after a return if .nil?' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' + + source = Solargraph::Source.load_string(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + c + return c if c.nil? + c + 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('String') + + clip = api_map.clip_at('test.rb', [7, 17]) + expect(clip.infer.to_s).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('String') + end + + it 'uses ! to detect nilness' do + pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_bang' + + source = Solargraph::Source.load_string(%( + class A + # @param a [Integer, nil] + # @return [Integer] + def foo a + return a unless !a + 123 + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 17]) + expect(clip.infer.to_s).to eq('Integer') + end end From 6a95643d39e385e2510df1b9bd87123dc21dafab Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 09:00:23 -0400 Subject: [PATCH 272/524] Rename method --- lib/solargraph/parser/flow_sensitive_typing.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 41ce6eeaf..d9f556e25 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -68,7 +68,7 @@ def process_if(if_node) get_node_end_position(then_clause)) end - process_conditional(conditional_node, true_ranges) + process_expression(conditional_node, true_ranges) end class << self @@ -162,7 +162,7 @@ def process_facts(facts_by_pin, presences) # @param true_ranges [Array] # # @return [void] - def process_conditional(conditional_node, true_ranges) + def process_expression(conditional_node, true_ranges) if conditional_node.type == :send process_isa(conditional_node, true_ranges) elsif conditional_node.type == :and From 84ac4edf9adea5a0cd6e5f3e42d6dee38a8aa830 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 09:26:01 -0400 Subject: [PATCH 273/524] Add false_presences arguments --- lib/solargraph/parser/flow_sensitive_typing.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index d9f556e25..f727dc0db 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -12,9 +12,10 @@ def initialize(locals, enclosing_breakable_pin = nil) # @param and_node [Parser::AST::Node] # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_and(and_node, true_ranges = []) + def process_and(and_node, true_ranges = [], false_ranges = []) # @type [Parser::AST::Node] lhs = and_node.children[0] # @type [Parser::AST::Node] @@ -25,7 +26,7 @@ def process_and(and_node, true_ranges = []) rhs_presence = Range.new(before_rhs_pos, get_node_end_position(rhs)) - process_isa(lhs, true_ranges + [rhs_presence]) + process_isa(lhs, true_ranges + [rhs_presence], []) end # @param if_node [Parser::AST::Node] @@ -50,6 +51,7 @@ def process_if(if_node) else_clause = if_node.children[2] true_ranges = [] + false_ranges = [] if always_breaks?(else_clause) unless enclosing_breakable_pin.nil? rest_of_breakable_body = Range.new(get_node_end_position(if_node), @@ -68,7 +70,7 @@ def process_if(if_node) get_node_end_position(then_clause)) end - process_expression(conditional_node, true_ranges) + process_expression(conditional_node, true_ranges, false_ranges) end class << self @@ -160,13 +162,14 @@ def process_facts(facts_by_pin, presences) # @param conditional_node [Parser::AST::Node] # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_expression(conditional_node, true_ranges) + def process_expression(conditional_node, true_ranges, false_ranges) if conditional_node.type == :send - process_isa(conditional_node, true_ranges) + process_isa(conditional_node, true_ranges, false_ranges) elsif conditional_node.type == :and - process_and(conditional_node, true_ranges) + process_and(conditional_node, true_ranges, false_ranges) end end @@ -208,9 +211,10 @@ def find_local(variable_name, position) # @param isa_node [Parser::AST::Node] # @param true_presences [Array] + # @param false_presences [Array] # # @return [void] - def process_isa(isa_node, true_presences) + def process_isa(isa_node, true_presences, false_presences) isa_type_name, variable_name = parse_isa(isa_node) return if variable_name.nil? || variable_name.empty? isa_position = Range.from_node(isa_node).start From 5753e8f344ce8e7b4d9ceef514c7f2ed77034e5a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 09:32:13 -0400 Subject: [PATCH 274/524] Generalize some methods for future expansion --- .../parser/flow_sensitive_typing.rb | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index f727dc0db..52e3cc45b 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -16,6 +16,8 @@ def initialize(locals, enclosing_breakable_pin = nil) # # @return [void] def process_and(and_node, true_ranges = [], false_ranges = []) + return unless and_node.type == :and + # @type [Parser::AST::Node] lhs = and_node.children[0] # @type [Parser::AST::Node] @@ -26,13 +28,32 @@ def process_and(and_node, true_ranges = [], false_ranges = []) rhs_presence = Range.new(before_rhs_pos, get_node_end_position(rhs)) - process_isa(lhs, true_ranges + [rhs_presence], []) + + # can't assume if an and is false that every single condition + # is false, so don't provide any false ranges to assert facts + # on + process_expression(lhs, true_ranges + [rhs_presence], []) + end + + # @param node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_calls(node, true_presences, false_presences) + return unless node.type == :send + + process_isa(node, true_presences, false_presences) end # @param if_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] # # @return [void] - def process_if(if_node) + def process_if(if_node, true_ranges = [], false_ranges = []) + return if if_node.type != :if + # # See if we can refine a type based on the result of 'if foo.nil?' # @@ -50,8 +71,6 @@ def process_if(if_node) # @type [Parser::AST::Node] else_clause = if_node.children[2] - true_ranges = [] - false_ranges = [] if always_breaks?(else_clause) unless enclosing_breakable_pin.nil? rest_of_breakable_body = Range.new(get_node_end_position(if_node), @@ -160,43 +179,53 @@ def process_facts(facts_by_pin, presences) end end - # @param conditional_node [Parser::AST::Node] + # @param expression_node [Parser::AST::Node] # @param true_ranges [Array] # @param false_ranges [Array] # # @return [void] - def process_expression(conditional_node, true_ranges, false_ranges) - if conditional_node.type == :send - process_isa(conditional_node, true_ranges, false_ranges) - elsif conditional_node.type == :and - process_and(conditional_node, true_ranges, false_ranges) - end + def process_expression(expression_node, true_ranges, false_ranges) + process_calls(expression_node, true_ranges, false_ranges) + process_and(expression_node, true_ranges, false_ranges) end - # @param isa_node [Parser::AST::Node] - # @return [Array(String, String), nil] - def parse_isa(isa_node) - return unless isa_node&.type == :send && isa_node.children[1] == :is_a? + # @param call_node [Parser::AST::Node] + # @param method_name [Symbol] + # @return [Array(String, String), nil] Tuple of rgument to + # function, then receiver of function if it's a variable, + # otherwise nil if no simple variable receiver + def parse_call(call_node, method_name) + return unless call_node&.type == :send && call_node.children[1] == method_name # Check if conditional node follows this pattern: # s(:send, # s(:send, nil, :foo), :is_a?, # s(:const, nil, :Baz)), - isa_receiver = isa_node.children[0] - isa_type_name = type_name(isa_node.children[2]) - return unless isa_type_name + # + call_receiver = call_node.children[0] + call_arg = type_name(call_node.children[2]) - # check if isa_receiver looks like this: + # check if call_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) - variable_name = isa_receiver.children[1].to_s + if call_receiver&.type == :send && call_receiver.children[0].nil? && call_receiver.children[1].is_a?(Symbol) + variable_name = call_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 = call_receiver.children[0].to_s if call_receiver&.type == :lvar return unless variable_name - [isa_type_name, variable_name] + [call_arg, variable_name] + end + + # @param isa_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_isa(isa_node) + call_type_name, variable_name = parse_call(isa_node, :is_a?) + + return unless call_type_name + + [call_type_name, variable_name] end # @param variable_name [String] From 9aea987119725d38007163be5dffd8cd6d4879eb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 10 Oct 2025 07:15:26 -0400 Subject: [PATCH 275/524] Handle simple 'if foo.nil?' cases --- .../parser/flow_sensitive_typing.rb | 41 +++++++++++++++++-- lib/solargraph/pin/local_variable.rb | 12 ++++++ spec/parser/flow_sensitive_typing_spec.rb | 4 -- spec/type_checker/levels/strong_spec.rb | 16 ++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 52e3cc45b..2e477b3fd 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -44,6 +44,7 @@ def process_calls(node, true_presences, false_presences) return unless node.type == :send process_isa(node, true_presences, false_presences) + process_nilp(node, true_presences, false_presences) end # @param if_node [Parser::AST::Node] @@ -81,7 +82,7 @@ def process_if(if_node, true_ranges = [], false_ranges = []) unless then_clause.nil? # - # Add specialized locals for the then clause range + # If the condition is true we can assume things about the then clause # before_then_clause_loc = then_clause.location.expression.adjust(begin_pos: -1) before_then_clause_pos = Position.new(before_then_clause_loc.line, before_then_clause_loc.column) @@ -171,9 +172,11 @@ def process_facts(facts_by_pin, presences) # facts_by_pin.each_pair do |pin, facts| facts.each do |fact| - downcast_type_name = fact.fetch(:type) + downcast_type_name = fact.fetch(:type, nil) + nilp = fact.fetch(:nil, nil) presences.each do |presence| - add_downcast_local(pin, downcast_type_name, presence) + add_downcast_local(pin, downcast_type_name, presence) unless downcast_type_name.nil? + add_downcast_local(pin, 'nil', presence) if nilp == true end end end @@ -257,6 +260,36 @@ def process_isa(isa_node, true_presences, false_presences) process_facts(if_true, true_presences) end + # @param nilp_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_nilp(nilp_node) + parse_call(nilp_node, :nil?) + end + + # @param nilp_node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_nilp(nilp_node, true_presences, false_presences) + nilp_arg, variable_name = parse_nilp(nilp_node) + return if variable_name.nil? || variable_name.empty? + # if .nil? got an argument, move on, this isn't the situation + # we're looking for and typechecking will cover any invalid + # ones + return unless nilp_arg.nil? + + nilp_position = Range.from_node(nilp_node).start + + pin = find_local(variable_name, nilp_position) + return unless pin + + if_true = {} + if_true[pin] ||= [] + if_true[pin] << { nil: true } + process_facts(if_true, true_presences) + end + # @param node [Parser::AST::Node] # # @return [String, nil] @@ -264,7 +297,9 @@ def type_name(node) # e.g., # s(:const, nil, :Baz) return unless node&.type == :const + # @type [Parser::AST::Node, nil] module_node = node.children[0] + # @type [Parser::AST::Node, nil] class_node = node.children[1] return class_node.to_s if module_node.nil? diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 9eae6cc6f..f6e6fe037 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -21,6 +21,18 @@ def initialize assignment: nil, presence: nil, presence_certain: false, **splat @presence_certain = presence_certain end + # @param api_map [ApiMap] + # @return [ComplexType] + def probe api_map + if presence_certain? && return_type&.defined? + # flow sensitive typing has already figured out this type + # has been downcast - use the type it figured out + return return_type.qualify(api_map, *gates) + end + + super + end + def combine_with(other, attrs={}) new_attrs = { assignment: assert_same(other, :assignment), diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 30b813d85..076b99b7a 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -318,8 +318,6 @@ def verify_repro(repr) end it 'uses nil? and && in a simple if() to refine nilness - nil? first' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] # @param throw_the_dice [Boolean] @@ -914,8 +912,6 @@ def bar(baz: nil) end it 'provides a useful pin after a return if .nil?' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class A # @param b [Hash{String => String}] diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 970435dc3..750108365 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,22 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'respects pin visibility' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param baz [Integer, nil] + # @return [Integer, nil] + def foo baz = 123 + return nil if baz.nil? + baz + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'does not complain on array dereference' do checker = type_checker(%( # @param idx [Integer, nil] an index From 82e5bbfdc082b89aded1681bc2e459eadc3734ed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 15:49:34 -0400 Subject: [PATCH 276/524] Handle 'if foo' in flow-sensitive typing --- .rubocop_todo.yml | 8 +-- .../parser/flow_sensitive_typing.rb | 51 ++++++++++++++++++- lib/solargraph/pin/base.rb | 1 + lib/solargraph/pin/local_variable.rb | 32 +++++++++++- spec/parser/flow_sensitive_typing_spec.rb | 30 +++++++++-- 5 files changed, 108 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83339e756..87011662b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -1278,12 +1277,7 @@ YARD/MismatchName: Enabled: false YARD/TagTypeSyntax: - Exclude: - - 'lib/solargraph/api_map/constants.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 2e477b3fd..7bbaf1e6d 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -142,11 +142,18 @@ def self.visible_pins(pins, name, closure, location) private # @param pin [Pin::LocalVariable] - # @param downcast_type_name [String] + # @param downcast_type_name [String, :not_nil] # @param presence [Range] # # @return [void] def add_downcast_local(pin, downcast_type_name, presence) + return_type = if downcast_type_name == :not_nil + pin.return_type + else + ComplexType.parse(downcast_type_name) + end + exclude_return_type = downcast_type_name == :not_nil ? ComplexType::NIL : nil + # @todo Create pin#update method new_pin = Solargraph::Pin::LocalVariable.new( location: pin.location, @@ -155,10 +162,12 @@ def add_downcast_local(pin, downcast_type_name, presence) assignment: pin.assignment, comments: pin.comments, presence: presence, - return_type: ComplexType.try_parse(downcast_type_name), + return_type: return_type, + exclude_return_type: exclude_return_type, presence_certain: true, source: :flow_sensitive_typing ) + new_pin.reset_generated! locals.push(new_pin) end @@ -174,9 +183,11 @@ def process_facts(facts_by_pin, presences) facts.each do |fact| downcast_type_name = fact.fetch(:type, nil) nilp = fact.fetch(:nil, nil) + not_nilp = fact.fetch(:not_nil, nil) presences.each do |presence| add_downcast_local(pin, downcast_type_name, presence) unless downcast_type_name.nil? add_downcast_local(pin, 'nil', presence) if nilp == true + add_downcast_local(pin, :not_nil, presence) if not_nilp == true end end end @@ -190,6 +201,7 @@ def process_facts(facts_by_pin, presences) def process_expression(expression_node, true_ranges, false_ranges) process_calls(expression_node, true_ranges, false_ranges) process_and(expression_node, true_ranges, false_ranges) + process_variable(expression_node, true_ranges, false_ranges) end # @param call_node [Parser::AST::Node] @@ -290,6 +302,41 @@ def process_nilp(nilp_node, true_presences, false_presences) process_facts(if_true, true_presences) end + # @param var_node [Parser::AST::Node] + # + # @return [String, nil] Variable name referenced + def parse_variable(var_node) + return if var_node.children.length != 1 + + var_node.children[0]&.to_s + end + + # @return [void] + # @param node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + def process_variable(node, true_presences, false_presences) + return unless [:lvar, :ivar, :cvar, :gvar].include?(node.type) + + variable_name = parse_variable(node) + return if variable_name.nil? + + var_position = Range.from_node(node).start + + pin = find_local(variable_name, var_position) + return unless pin + + if_true = {} + if_true[pin] ||= [] + if_true[pin] << { not_nil: true } + process_facts(if_true, true_presences) + + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { nil: true } + process_facts(if_false, false_presences) + end + # @param node [Parser::AST::Node] # # @return [String, nil] diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 511c7deb7..dae997f89 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -572,6 +572,7 @@ def proxy return_type result = dup result.return_type = return_type result.proxied = true + result.reset_generated! result end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index f6e6fe037..f87031ae1 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -13,12 +13,40 @@ def presence_certain? # @param assignment [AST::Node, nil] # @param presence [Range, nil] # @param presence_certain [Boolean] + # @param exclude_return_type [ComplexType, nil] Ensure any return + # type returned will never include these unique types in the + # unique types of its complex type # @param splat [Hash] - def initialize assignment: nil, presence: nil, presence_certain: false, **splat + def initialize assignment: nil, presence: nil, presence_certain: false, exclude_return_type: nil, + **splat super(**splat) @assignment = assignment @presence = presence @presence_certain = presence_certain + @exclude_return_type = exclude_return_type + end + + def reset_generated! + @return_type_minus_exclusions = nil + super + end + + # @return [ComplexType, nil] + def return_type + return_type_minus_exclusions(@return_type || generate_complex_type) + end + + # @param raw_return_type [ComplexType, nil] + # @return [ComplexType, nil] + def return_type_minus_exclusions(raw_return_type) + @return_type_minus_exclusions ||= + if exclude_return_type && raw_return_type + types = raw_return_type.items - exclude_return_type.items + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + else + raw_return_type + end end # @param api_map [ApiMap] @@ -60,6 +88,8 @@ def to_rbs private + attr_reader :exclude_return_type + # @param tag1 [String] # @param tag2 [String] # @return [Boolean] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 076b99b7a..77785bc69 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -212,9 +212,33 @@ class Repro < ReproBase; end expect(clip.infer.to_s).to eq('Float') end - it 'uses varname in a "break unless" statement in a while to refine types' do - pending 'lib/solargraph/pin/breakable.rb' + it 'uses varname in a simple if()' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + pending('supporting else after if on varname') + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname in a "break unless" statement in a while to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -490,8 +514,6 @@ def verify_repro(repr) end it 'uses varname and && in a simple if() - varname first' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] # @param throw_the_dice [Boolean] From a30c1e6eb8769984d80bf546c54d16e0bcf7713e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 16:22:51 -0400 Subject: [PATCH 277/524] Set up flow-sensitive typing specs, currently passing and pending --- spec/parser/node_methods_spec.rb | 10 +- spec/pin/parameter_spec.rb | 30 ++ spec/source/chain/call_spec.rb | 54 +++ spec/source/chain/or_spec.rb | 35 ++ spec/source/chain/q_call_spec.rb | 23 ++ spec/type_checker/levels/alpha_spec.rb | 79 +++++ spec/type_checker/levels/strict_spec.rb | 134 +++++++- spec/type_checker/levels/strong_spec.rb | 431 +++++++++++++++++++++--- spec/type_checker/levels/typed_spec.rb | 57 ++++ 9 files changed, 797 insertions(+), 56 deletions(-) create mode 100644 spec/source/chain/or_spec.rb create mode 100644 spec/source/chain/q_call_spec.rb create mode 100644 spec/type_checker/levels/alpha_spec.rb diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index f9504b584..e5cd0c78f 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -185,11 +185,11 @@ end it "handles top 'or' nodes" do + pending 'flow-sensitive typing improvements' + node = Solargraph::Parser.parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.length).to eq(2) - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[0])).to eq('::Integer') - expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[1])).to eq('::String') + expect(rets.length).to eq(1) end it "handles nested 'and' nodes" do @@ -336,9 +336,11 @@ end it "handles top 'or' nodes" do + pending 'flow-sensitive typing improvements' + node = Solargraph::Parser.parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) - expect(rets.map(&:type)).to eq([:int, :str]) + expect(rets.map(&:type)).to eq([:or]) end it "handles nested 'and' nodes from return" do diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 082ec54c6..8c3949a5c 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -473,5 +473,35 @@ def self.foo bar: 'bar' type = pin.probe(api_map) expect(type.simple_tags).to eq('String') end + + it 'handles a relative type name case' do + pending 'flow-sensitive typing improvements' + + source = Solargraph::Source.load_string(%( + module A + module B + class Method + end + end + end + + module A + module B + class C < B::Method + # @param alt [Method] + # @return [B::Method, nil] + def resolve_method alt + alt + end + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map(source) + + clip = api_map.clip_at('test.rb', [14, 16]) + expect(clip.infer.rooted_tags).to eq('::A::B::Method') + end end end diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 8b67a3c66..1ffa70b38 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -371,6 +371,21 @@ def yielder(&blk) expect(type.tag).to eq('Enumerator>') end + it 'allows calls off of nilable objects by default' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 6)) + type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + expect(type.tag).to eq('String') + end + it 'calculates class return type based on class generic' do source = Solargraph::Source.load_string(%( # @generic A @@ -627,4 +642,43 @@ def bl clip = api_map.clip_at('test.rb', [3, 8]) expect(clip.infer.rooted_tags).to eq('::String') end + + it 'sends proper gates in ProxyType' do + pending 'Proxytype improvements' + + source = Solargraph::Source.load_string(%( + module Foo + module Bar + class Symbol + end + end + end + + module Foo + module Baz + class Quux + # @return [void] + def foo + s = objects_by_class(Bar::Symbol) + s + end + + # @generic T + # @param klass [Class>] + # @return [Set>] + def objects_by_class klass + # @type [Set>] + s = Set.new + s + end + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + + clip = api_map.clip_at('test.rb', [14, 14]) + expect(clip.infer.rooted_tags).to eq('::Set<::Foo::Bar::Symbol>') + end end diff --git a/spec/source/chain/or_spec.rb b/spec/source/chain/or_spec.rb new file mode 100644 index 000000000..85019a0b7 --- /dev/null +++ b/spec/source/chain/or_spec.rb @@ -0,0 +1,35 @@ +describe Solargraph::Source::Chain::Or do + it 'handles simple nil-removal' do + pending 'flow-sensitive typing improvements' + + source = Solargraph::Source.load_string(%( + # @param a [Integer, nil] + def foo a + b = a || 10 + b + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.simplify_literals.rooted_tags).to eq('::Integer') + end + + it 'removes nil from more complex cases' do + pending 'flow-sensitive typing improvements' + + source = Solargraph::Source.load_string(%( + def foo + out = ENV['BAR'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + out + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [3, 8]) + expect(clip.infer.simplify_literals.rooted_tags).to eq('::String') + end +end diff --git a/spec/source/chain/q_call_spec.rb b/spec/source/chain/q_call_spec.rb new file mode 100644 index 000000000..a63568358 --- /dev/null +++ b/spec/source/chain/q_call_spec.rb @@ -0,0 +1,23 @@ +describe Solargraph::Source::Chain::QCall do + it 'understands &. in chains' do + source = Solargraph::Source.load_string(%( + # @param a [String, nil] + # @return [String, nil] + def foo a + b = a&.upcase + b + end + + b = foo 123 + b + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.to_s).to eq('String, nil') + + clip = api_map.clip_at('test.rb', [9, 6]) + expect(clip.infer.to_s).to eq('String, nil') + end +end diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb new file mode 100644 index 000000000..d6c58df00 --- /dev/null +++ b/spec/type_checker/levels/alpha_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +describe Solargraph::TypeChecker do + context 'when at alpha level' do + def type_checker code + Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + end + + it 'allows a compatible function call from two distinct types in a union' do + checker = type_checker(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz.nil? + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not falsely enforce nil in return types' do + pending 'flow-sensitive typing improvements' + + checker = type_checker(%( + # @return [Integer] + def foo + # @sg-ignore + # @type [Integer, nil] + a = bar + a || 123 + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'reports nilable type issues' do + pending 'flow-sensitive typing improvements' + + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: a expected String, received String, nil']) + end + + it 'tracks type of ivar' do + checker = type_checker(%( + class Foo + # @return [void] + def initialize + @sync_count = 0 + end + + # @return [void] + def synchronized? + @sync_count < 2 + end + + # @return [void] + def catalog + @sync_count += 1 + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + end +end diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 47bf45a2c..ea2cdccf3 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -5,6 +5,36 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strict) end + it 'can derive return types' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'ignores nilable type issues' do + checker = type_checker(%( + # @param a [String] + # @return [void] + def foo(a); end + + # @param b [String, nil] + # @return [void] + def bar(b) + foo(b) + end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'handles compatible interfaces with self types on call' do checker = type_checker(%( # @param a [Enumerable] @@ -61,7 +91,7 @@ def bar(a); end require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.load_with_cache('.', $stdout) + api_map = Solargraph::ApiMap.load '.' 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 @@ -581,6 +611,46 @@ def bar expect(checker.problems).to be_empty end + it 'Can infer through simple ||= on ivar' do + checker = type_checker(%( + class Foo + def recipient + @recipient ||= true + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on lvar' do + checker = type_checker(%( + def recipient + recip ||= true + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on cvar' do + checker = type_checker(%( + class Foo + def recipient + @@recipient ||= true + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'Can infer through simple ||= on civar' do + checker = type_checker(%( + class Foo + @recipient ||= true + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'Can infer through ||= with a begin+end' do checker = type_checker(%( def recipient @@ -701,6 +771,19 @@ def test(foo: nil) expect(checker.problems).to be_empty end + + it 'validates parameters in function calls' do + checker = type_checker(%( + # @param bar [String] + def foo(bar); end + + def baz + foo(123) + end + )) + expect(checker.problems.map(&:message)).to eq(['Wrong argument type for #foo: bar expected String, received 123']) + end + it 'validates inferred return types with complex tags' do checker = type_checker(%( # @param foo [Numeric, nil] a foo @@ -769,19 +852,6 @@ def meth(param1) expect(checker.problems.map(&:message)).to eq(['Unresolved call to upcase']) end - it 'does not falsely enforce nil in return types' do - checker = type_checker(%( - # @return [Integer] - def foo - # @sg-ignore - # @type [Integer, nil] - a = bar - a || 123 - end - )) - expect(checker.problems.map(&:message)).to be_empty - end - it 'refines types on is_a? and && to downcast and avoid false positives' do checker = type_checker(%( def foo @@ -1004,11 +1074,47 @@ def bar 123 elsif rand 456 + else + nil end end end )) expect(checker.problems.map(&:message)).to eq([]) end + + it 'does not complain on defaulted reader with un-elsed if' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + if rand + 123 + elsif rand + 456 + end + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + + it 'does not complain on defaulted reader with with un-elsed unless' do + checker = type_checker(%( + class Foo + # @return [Integer, nil] + def bar + @bar ||= + unless rand + 123 + end + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 750108365..2b106aec6 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,109 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'does not misunderstand types during flow-sensitive typing' do + checker = type_checker(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + return if c.nil? + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/nil? pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + return 123 if bar.nil? + bar + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects || overriding nilable types' do + checker = type_checker(%( + # @return [String] + def global_config_path + ENV['SOLARGRAPH_GLOBAL_CONFIG'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'is able to probe type over an assignment' do + checker = type_checker(%( + # @return [String] + def global_config_path + out = 'foo' + out + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/foo pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + baz = bar + return baz if baz + 123 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles a flow sensitive typing if correctly' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo a = nil + b = a + if b + b.upcase + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles another flow sensitive typing if correctly' do + checker = type_checker(%( + class A + # @param e [String] + # @param f [String] + # @return [void] + def d(e, f:); end + + # @return [void] + def a + c = rand ? nil : "foo" + if c + d(c, f: c) + end + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'respects pin visibility' do checker = type_checker(%( class Foo @@ -22,7 +125,7 @@ def foo baz = 123 it 'does not complain on array dereference' do checker = type_checker(%( - # @param idx [Integer, nil] an index + # @param idx [Integer] an index # @param arr [Array] an array of integers # # @return [void] @@ -33,6 +136,23 @@ def foo(idx, arr) expect(checker.problems.map(&:message)).to be_empty end + it 'understands local evaluation with ||= removes nil from lhs type' do + checker = type_checker(%( + class Foo + def initialize + @bar = nil + end + + # @return [Integer] + def bar + @bar ||= 123 + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + it 'complains on bad @type assignment' do checker = type_checker(%( # @type [Integer] @@ -83,21 +203,6 @@ def bar; end expect(checker.problems.first.message).to include('Missing @return tag') end - it 'ignores nilable type issues' do - checker = type_checker(%( - # @param a [String] - # @return [void] - def foo(a); end - - # @param b [String, nil] - # @return [void] - def bar(b) - foo(b) - end - )) - expect(checker.problems.map(&:message)).to eq([]) - end - it 'calls out keyword issues even when required arg count matches' do checker = type_checker(%( # @param a [String] @@ -146,21 +251,6 @@ def bar expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') end - it 'calls out missing args after a defaulted param' do - checker = type_checker(%( - # @param a [String] - # @param b [String] - # @return [void] - def foo(a = 'foo', b); end - - # @return [void] - def bar - foo(123) - end - )) - expect(checker.problems.map(&:message)).to include('Not enough arguments to #foo') - end - it 'reports missing param tags' do checker = type_checker(%( class Foo @@ -272,6 +362,149 @@ def bar &block expect(checker.problems).to be_empty end + it 'does not need fully specified container types' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failure with no generic tag' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class>] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores undefined resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures from current Solargraph limitation' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures with only one arg' do + checker = type_checker(%( + # @generic T + # @param path [String] + # @param klass [Class>] + # @return [void] + def code_object_at path, klass = Integer + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on select { is_a? } pattern' do + checker = type_checker(%( + # @param arr [Enumerable} + # @return [Enumerable] + def downcast_arr(arr) + arr.select { |pin| pin.is_a?(Integer) } + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via return value' do + checker = type_checker(%( + # @param bar [Integer] + # @return [Integer, nil] + def foo(bar) + bar + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via select' do + checker = type_checker(%( + # @return [Float, nil]} + def bar; rand; end + + # @param arr [Enumerable} + # @return [Integer, nil] + def downcast_arr(arr) + # @type [Object, nil] + foo = arr.select { |pin| pin.is_a?(Integer) && bar }.last + foo + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'inherits param tags from superclass methods' do checker = type_checker(%( class Foo @@ -289,6 +522,20 @@ def meth arg expect(checker.problems).to be_empty end + it 'understands Open3 methods' do + checker = type_checker(%( + require 'open3' + + # @return [void] + def run_command + # @type [Hash{String => String}] + foo = {'foo' => 'bar'} + Open3.capture2e(foo, 'ls', chdir: '/tmp') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'resolves constants inside modules inside classes' do checker = type_checker(%( class Bar @@ -309,18 +556,126 @@ def baz expect(checker.problems.map(&:message)).to be_empty end - it 'understands Open3 methods' do - checker = type_checker(%( - require 'open3' + context 'with class name available in more than one gate' do + let(:checker) do + type_checker(%( + module Foo + module Bar + class Symbol + end + end + end + + module Foo + module Baz + class Quux + # @return [void] + def foo + objects_by_class(Bar::Symbol) + end + + # @generic T + # @param klass [Class>] + # @return [Set>] + def objects_by_class klass + # @type [Set>] + s = Set.new + s + end + end + end + end + )) + end + it 'resolves class name correctly in generic resolution' do + expect(checker.problems.map(&:message)).to be_empty + end + end + + it 'handles "while foo" flow sensitive typing correctly' do + checker = type_checker(%( + # @param a [String, nil] # @return [void] - def run_command - # @type [Hash{String => String}] - foo = {'foo' => 'bar'} - Open3.capture2e(foo, 'ls', chdir: '/tmp') + def foo a = nil + b = a + while b + b.upcase + b = nil if rand > 0.5 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does flow sensitive typing even inside a block' do + checker = type_checker(%( + class Quux + # @param foo [String, nil] + # + # @return [void] + def baz(foo) + bar = foo + [].each do + bar.upcase unless bar.nil? + end + end + end)) + + expect(checker.problems.map(&:location).map(&:range).map(&:start)).to be_empty + end + + it 'accepts ivar assignments and references with no intermediate calls as safe' do + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + out = @foo.round + twiddle + out + end end )) + expect(checker.problems.map(&:message)).to be_empty end + + it 'knows that ivar references with intermediate calls are not safe' do + pending 'flow-sensitive typing improvements' + + checker = type_checker(%( + class Foo + def initialize + # @type [Integer, nil] + @foo = nil + end + + # @return [void] + def twiddle + @foo = nil if rand if rand > 0.5 + end + + # @return [Integer] + def bar + @foo = 123 + twiddle + @foo.round + end + end + )) + + expect(checker.problems.map(&:message)).to eq(["Foo#bar return type could not be inferred", "Unresolved call to round"]) + end end end diff --git a/spec/type_checker/levels/typed_spec.rb b/spec/type_checker/levels/typed_spec.rb index b2071465e..46d854f3e 100644 --- a/spec/type_checker/levels/typed_spec.rb +++ b/spec/type_checker/levels/typed_spec.rb @@ -4,6 +4,23 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :typed) end + it 'respects pin visibility' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Array] + # @return [Symbol, Integer] + def foo bar + baz = bar.first + return 123 if baz.nil? + baz + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'reports mismatched types for empty methods' do checker = type_checker(%( class Foo @@ -38,6 +55,21 @@ def bar expect(checker.problems.first.message).to include('does not match') end + it 'reports mismatched key and subtypes' do + pending 'generic typechecking improvements' + + checker = type_checker(%( + # @return [Hash{String => String}] + def foo + # @type h [Hash{Integer => String}] + h = {} + h + end + )) + expect(checker.problems).to be_one + expect(checker.problems.first.message).to include('does not match') + end + it 'reports mismatched inherited return tags' do checker = type_checker(%( class Sup @@ -189,6 +221,31 @@ def foo expect(checker.problems).to be_empty end + it 'validates default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 123); end + )) + expect(checker.problems.map(&:message)) + .to eq(['Declared type String does not match inferred type 123 for variable bar']) + end + + it 'validates string default values of parameters' do + checker = type_checker(%( + # @param bar [String] + def foo(bar = 'foo'); end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'validates symbol default values of parameters' do + checker = type_checker(%( + # @param bar [Symbol] + def foo(bar = :baz); end + )) + expect(checker.problems.map(&:message)).to eq([]) + end + it 'validates subclass arguments of param types' do checker = type_checker(%( class Sup From c6c96e3bde34f2deec95e0fe398dd3c31837b279 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 17:03:07 -0400 Subject: [PATCH 278/524] Remove nilness in 'foo unless foo.nil? || foo' --- .../parser/flow_sensitive_typing.rb | 35 +++++++++++++++++++ spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 7bbaf1e6d..c8af32b81 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -35,6 +35,35 @@ def process_and(and_node, true_ranges = [], false_ranges = []) process_expression(lhs, true_ranges + [rhs_presence], []) end + # @param or_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] + # + # @return [void] + def process_or(or_node, true_ranges = [], false_ranges = []) + return unless or_node.type == :or + + # @type [Parser::AST::Node] + lhs = or_node.children[0] + # @type [Parser::AST::Node] + rhs = or_node.children[1] + + before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1) + before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column) + + rhs_presence = Range.new(before_rhs_pos, + get_node_end_position(rhs)) + + # can assume if an or is false that every single condition is + # false, so't provide false ranges to assert facts on + + # can't assume if an or is true that every single condition is + # true, so don't provide true ranges to assert facts on + + process_expression(lhs, [], [rhs_presence]) + process_expression(rhs, [], []) + end + # @param node [Parser::AST::Node] # @param true_presences [Array] # @param false_presences [Array] @@ -201,6 +230,7 @@ def process_facts(facts_by_pin, presences) def process_expression(expression_node, true_ranges, false_ranges) process_calls(expression_node, true_ranges, false_ranges) process_and(expression_node, true_ranges, false_ranges) + process_or(expression_node, true_ranges, false_ranges) process_variable(expression_node, true_ranges, false_ranges) end @@ -300,6 +330,11 @@ def process_nilp(nilp_node, true_presences, false_presences) if_true[pin] ||= [] if_true[pin] << { nil: true } process_facts(if_true, true_presences) + + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { not_nil: true } + process_facts(if_false, false_presences) end # @param var_node [Parser::AST::Node] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 77785bc69..80b2c7b72 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -492,8 +492,6 @@ def verify_repro(repr, throw_the_dice) end it 'uses .nil? and or in an unless' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [String, nil] # @param throw_the_dice [Boolean] From ab404c85e8144fd632dbfcfa6eceb914d1e20113 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 17:09:04 -0400 Subject: [PATCH 279/524] Handle foo if unrelated && foo --- lib/solargraph/parser/flow_sensitive_typing.rb | 1 + spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index c8af32b81..18f281196 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -33,6 +33,7 @@ def process_and(and_node, true_ranges = [], false_ranges = []) # is false, so don't provide any false ranges to assert facts # on process_expression(lhs, true_ranges + [rhs_presence], []) + process_expression(rhs, true_ranges, []) end # @param or_node [Parser::AST::Node] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 80b2c7b72..b332c6b6f 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -366,8 +366,6 @@ def verify_repro(repr, throw_the_dice) end it 'uses nil? and && in a simple if() to refine nilness - nil? second' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] # @param throw_the_dice [Boolean] From b20ad1e74853d440caf15218fca8e552f31ec596 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 17:44:43 -0400 Subject: [PATCH 280/524] Handle 'break if foo.nil?' --- lib/solargraph/parser/flow_sensitive_typing.rb | 13 +++++++++---- spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 18f281196..9374a5d76 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -102,10 +102,15 @@ def process_if(if_node, true_ranges = [], false_ranges = []) # @type [Parser::AST::Node] else_clause = if_node.children[2] - if always_breaks?(else_clause) - unless enclosing_breakable_pin.nil? - rest_of_breakable_body = Range.new(get_node_end_position(if_node), - get_node_end_position(enclosing_breakable_pin.node)) + unless enclosing_breakable_pin.nil? + rest_of_breakable_body = Range.new(get_node_end_position(if_node), + get_node_end_position(enclosing_breakable_pin.node)) + + if always_breaks?(then_clause) + false_ranges << rest_of_breakable_body + end + + if always_breaks?(else_clause) true_ranges << rest_of_breakable_body end end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index b332c6b6f..33e667d3c 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -255,8 +255,6 @@ class Repro < ReproBase; end end it 'uses varname in a "break if" statement in a while to refine types' do - pending 'lib/solargraph/pin/breakable.rb' - source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end From b0a7294893a32d8c049a5f69461a6ecc013fdf18 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 17:50:59 -0400 Subject: [PATCH 281/524] Handle 'if foo ... else foo' --- lib/solargraph/parser/flow_sensitive_typing.rb | 10 ++++++++++ spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 9374a5d76..e2a1ad1ae 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -125,6 +125,16 @@ def process_if(if_node, true_ranges = [], false_ranges = []) get_node_end_position(then_clause)) end + unless else_clause.nil? + # + # If the condition is true we can assume things about the else clause + # + before_else_clause_loc = else_clause.location.expression.adjust(begin_pos: -1) + before_else_clause_pos = Position.new(before_else_clause_loc.line, before_else_clause_loc.column) + false_ranges << Range.new(before_else_clause_pos, + get_node_end_position(else_clause)) + end + process_expression(conditional_node, true_ranges, false_ranges) end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 33e667d3c..176ffabec 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -232,8 +232,6 @@ def verify_repro(repr, throw_the_dice) clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - pending('supporting else after if on varname') - clip = api_map.clip_at('test.rb', [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end From 9cba783cca9c8adca5da8dd7420fe1e9f167fd84 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 19:20:52 -0400 Subject: [PATCH 282/524] Refactor --- lib/solargraph/parser/node_processor/base.rb | 11 +++++++++++ .../parser/parser_gem/node_processors/and_node.rb | 2 -- .../parser/parser_gem/node_processors/if_node.rb | 4 ---- lib/solargraph/pin/breakable.rb | 3 +++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index fad31e95b..e220bdefb 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -40,6 +40,17 @@ def process private + # @return [Solargraph::Position] + def position + Position.new(node.loc.line, node.loc.column) + end + + # @sg-ignore downcast output of Enumerable#select + # @return [Solargraph::Pin::Breakable, nil] + def enclosing_breakable_pin + pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position)}.last + end + # @param subregion [Region] # @return [void] def process_children subregion = region diff --git a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb index d3485af7c..b270af5b3 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb @@ -10,8 +10,6 @@ class AndNode < Parser::NodeProcessor::Base def process process_children - position = get_node_start_position(node) - enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_and(node) end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index 2452b9cc5..afb2be11b 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -10,10 +10,6 @@ class IfNode < Parser::NodeProcessor::Base def process process_children - position = get_node_start_position(node) - # @sg-ignore - # @type [Solargraph::Pin::Breakable, nil] - enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) end end diff --git a/lib/solargraph/pin/breakable.rb b/lib/solargraph/pin/breakable.rb index 05907b1bb..9e85fc081 100644 --- a/lib/solargraph/pin/breakable.rb +++ b/lib/solargraph/pin/breakable.rb @@ -4,6 +4,9 @@ module Pin module Breakable # @return [Parser::AST::Node] attr_reader :node + + # @return [Location, nil] + attr_reader :location end end end From 3e678e1e0ef904b32dbd2a235da240688a910cf8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 19:30:40 -0400 Subject: [PATCH 283/524] Support 'if foo.nil?' when 'foo' has more than one lvar pin Choose the first one to define the type to assume --- lib/solargraph/parser/flow_sensitive_typing.rb | 1 - spec/parser/flow_sensitive_typing_spec.rb | 4 ---- 2 files changed, 5 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index e2a1ad1ae..2eaadb838 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -295,7 +295,6 @@ def parse_isa(isa_node) # @return [Solargraph::Pin::LocalVariable, nil] def find_local(variable_name, position) pins = locals.select { |pin| pin.name == variable_name && pin.presence.include?(position) } - return unless pins.length == 1 pins.first end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 176ffabec..0dc132d7d 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -311,8 +311,6 @@ def baz; end end it 'uses nil? in a simple if() to refine nilness' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] def verify_repro(repr) @@ -558,8 +556,6 @@ def verify_repro(repr, throw_the_dice) end it 'uses variable in a simple if() to refine types' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] def verify_repro(repr) From 7b7c6403bba3cee6bce3497165164ba3e9765a4c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 11 Oct 2025 23:59:12 -0400 Subject: [PATCH 284/524] Support 'return if foo.nil?' --- .../parser/flow_sensitive_typing.rb | 30 ++++++++++- lib/solargraph/parser/node_processor/base.rb | 6 +++ .../parser_gem/node_processors/and_node.rb | 4 +- .../parser_gem/node_processors/if_node.rb | 4 +- lib/solargraph/pin.rb | 2 + lib/solargraph/pin/breakable.rb | 5 +- lib/solargraph/pin/compound_statementable.rb | 50 +++++++++++++++++++ lib/solargraph/pin/if.rb | 18 +++++++ spec/parser/flow_sensitive_typing_spec.rb | 4 -- 9 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 lib/solargraph/pin/compound_statementable.rb create mode 100644 lib/solargraph/pin/if.rb diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 2eaadb838..4ae817e0b 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -5,9 +5,11 @@ class FlowSensitiveTyping # @param locals [Array] # @param enclosing_breakable_pin [Solargraph::Pin::Breakable, nil] - def initialize(locals, enclosing_breakable_pin = nil) + # @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatementable, nil] + def initialize(locals, enclosing_breakable_pin, enclosing_compound_statement_pin) @locals = locals @enclosing_breakable_pin = enclosing_breakable_pin + @enclosing_compound_statement_pin = enclosing_compound_statement_pin end # @param and_node [Parser::AST::Node] @@ -115,6 +117,24 @@ def process_if(if_node, true_ranges = [], false_ranges = []) end end + unless enclosing_compound_statement_pin.nil? + rest_of_returnable_body = Range.new(get_node_end_position(if_node), + get_node_end_position(enclosing_compound_statement_pin.node)) + + # + # if one of the clauses always leaves the compound + # statement, we can assume things about the rest of the + # compound statement + # + if always_leaves_compound_statement?(then_clause) + false_ranges << rest_of_returnable_body + end + + if always_leaves_compound_statement?(else_clause) + true_ranges << rest_of_returnable_body + end + end + unless then_clause.nil? # # If the condition is true we can assume things about the then clause @@ -412,9 +432,15 @@ def always_breaks?(clause_node) clause_node&.type == :break end + # @param clause_node [Parser::AST::Node, nil] + def always_leaves_compound_statement?(clause_node) + # https://docs.ruby-lang.org/en/2.2.0/keywords_rdoc.html + [:return, :raise, :next, :redo, :retry].include?(clause_node&.type) + end + attr_reader :locals - attr_reader :enclosing_breakable_pin + attr_reader :enclosing_breakable_pin, :enclosing_compound_statement_pin end end end diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index e220bdefb..4d4123d9d 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -51,6 +51,12 @@ def enclosing_breakable_pin pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position)}.last end + # @sg-ignore downcast output of Enumerable#select + # @return [Solargraph::Pin::CompoundStatementable, nil] + def enclosing_compound_statement_pin + pins.select{|pin| pin.is_a?(Pin::CompoundStatementable) && pin.location&.range&.contain?(position)}.last + end + # @param subregion [Region] # @return [void] def process_children subregion = region diff --git a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb index b270af5b3..085c0c68a 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb @@ -10,7 +10,9 @@ class AndNode < Parser::NodeProcessor::Base def process process_children - FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_and(node) + FlowSensitiveTyping.new(locals, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_and(node) end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index afb2be11b..e59fa4baa 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -10,7 +10,9 @@ class IfNode < Parser::NodeProcessor::Base def process process_children - FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node) + FlowSensitiveTyping.new(locals, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_if(node) end end end diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 526ac6fc3..016550966 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -38,6 +38,8 @@ module Pin autoload :Until, 'solargraph/pin/until' autoload :While, 'solargraph/pin/while' autoload :Callable, 'solargraph/pin/callable' + autoload :CompoundStatementable, + 'solargraph/pin/compound_statementable' ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil, source: :pin_rb) end diff --git a/lib/solargraph/pin/breakable.rb b/lib/solargraph/pin/breakable.rb index 9e85fc081..5de67cf4a 100644 --- a/lib/solargraph/pin/breakable.rb +++ b/lib/solargraph/pin/breakable.rb @@ -1,7 +1,10 @@ module Solargraph module Pin - # Mix-in for pins which enclose code which the 'break' statement works with-in - e.g., blocks, when, until, ... + # Mix-in for pins which enclose code which the 'break' statement + # works with-in - e.g., blocks, when, until, ... module Breakable + include CompoundStatementable + # @return [Parser::AST::Node] attr_reader :node diff --git a/lib/solargraph/pin/compound_statementable.rb b/lib/solargraph/pin/compound_statementable.rb new file mode 100644 index 000000000..debad6615 --- /dev/null +++ b/lib/solargraph/pin/compound_statementable.rb @@ -0,0 +1,50 @@ +module Solargraph + module Pin + # A series of statements where if a given statement executes, /all + # of the previous statements in the sequence must have executed as + # well/. In other words, the statements are run from the top in + # sequence, until interrupted by something like a + # return/break/next/raise/etc. + # + # This mix-in is used in flow sensitive typing to determine how + # far we can assume a given assertion about a type can be trusted + # to be true. + # + # Some examples in Ruby: + # + # * Bodies of methods and Ruby blocks + # * Branches of conditionals and loops - if/elsif/else, + # unless/else, when, until, ||=, ?:, switch/case/else + # * The body of begin-end/try/rescue/ensure statements + # + # Compare/contrast with: + # + # * Scope - a sequence where variables declared are not available + # after the end of the scope. Note that this is not necessarily + # true for a compound statement. + # * Compound statement - synonym + # * Block - in Ruby this has a special meaning (a closure passed to a method), but + # in general parlance this is also a synonym. + # * Closure - a sequence which is also a scope + # * Namespace - a named sequence which is also a scope and a + # closure + # + # See: + # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf + # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html + # https://en.wikipedia.org/wiki/Block_(programming) + # + # Note: + # + # Just because statement #1 in a sequence is executed, it doesn't + # mean that future ones will. Consider the effect of + # break/next/return/raise/etc. on control flow. + module CompoundStatementable + # @return [Parser::AST::Node] + attr_reader :node + + # @return [Location, nil] + attr_reader :location + end + end +end diff --git a/lib/solargraph/pin/if.rb b/lib/solargraph/pin/if.rb new file mode 100644 index 000000000..35b8a9bfc --- /dev/null +++ b/lib/solargraph/pin/if.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Solargraph + module Pin + class If < Base + include CompoundStatementable + + # @param receiver [Parser::AST::Node, nil] + # @param node [Parser::AST::Node, nil] + # @param context [ComplexType, nil] + # @param args [::Array] + def initialize node: nil, **splat + super(**splat) + @node = node + end + end + end +end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 0dc132d7d..27c34cb93 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -709,8 +709,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a while to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -734,8 +732,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in an until to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] From 88156b4c4c207dfd9628911f1049505c75897c83 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 11:38:45 -0400 Subject: [PATCH 285/524] Support 'return foo unless !foo' --- .../parser/flow_sensitive_typing.rb | 25 +++++++++++++++++++ spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 4ae817e0b..3ded6010a 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -77,6 +77,7 @@ def process_calls(node, true_presences, false_presences) process_isa(node, true_presences, false_presences) process_nilp(node, true_presences, false_presences) + process_bang(node, true_presences, false_presences) end # @param if_node [Parser::AST::Node] @@ -372,6 +373,30 @@ def process_nilp(nilp_node, true_presences, false_presences) process_facts(if_false, false_presences) end + # @param bang_node [Parser::AST::Node] + # @return [Array(String, String), nil] + def parse_bang(bang_node) + parse_call(bang_node, :!) + end + + # @param bang_node [Parser::AST::Node] + # @param true_presences [Array] + # @param false_presences [Array] + # + # @return [void] + def process_bang(bang_node, true_presences, false_presences) + # pry(main)> require 'parser/current'; Parser::CurrentRuby.parse("!2") + # => s(:send, + # s(:int, 2), :!) + # end + return unless bang_node.type == :send && bang_node.children[1] == :! + + receiver = bang_node.children[0] + + # swap the two presences + process_expression(receiver, false_presences, true_presences) + end + # @param var_node [Parser::AST::Node] # # @return [String, nil] Variable name referenced diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 27c34cb93..8bd6ae3f6 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -944,8 +944,6 @@ def a b end it 'uses ! to detect nilness' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_bang' - source = Solargraph::Source.load_string(%( class A # @param a [Integer, nil] From 5d1e9aeaaeba8357a47fc0f9a487af40869cafe1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 12:21:41 -0400 Subject: [PATCH 286/524] Support 'return if foo.nil?' --- .rubocop_todo.yml | 8 +++++++- lib/solargraph/pin/method.rb | 1 + spec/parser/flow_sensitive_typing_spec.rb | 8 -------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 87011662b..aef9fa8fc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -468,6 +468,7 @@ Metrics/ClassLength: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/type_checker.rb' @@ -1165,7 +1166,12 @@ Style/SlicingWithRange: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. Style/SoleNestedConditional: - Enabled: false + Exclude: + - 'lib/solargraph/complex_type/unique_type.rb' + - 'lib/solargraph/pin/parameter.rb' + - 'lib/solargraph/source.rb' + - 'lib/solargraph/source/source_chainer.rb' + - 'lib/solargraph/type_checker.rb' # This cop supports safe autocorrection (--autocorrect). Style/StderrPuts: diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 011f096f6..871708253 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -6,6 +6,7 @@ module Pin # class Method < Callable include Solargraph::Parser::NodeMethods + include CompoundStatementable # @return [::Symbol] :public, :private, or :protected attr_reader :visibility diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 8bd6ae3f6..c5f443d8e 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -605,8 +605,6 @@ def verify_repro(repr = nil) end it 'uses .nil? in a return if() in an if to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -629,8 +627,6 @@ def bar(baz: nil) # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html it 'uses .nil? in a return if() in a method to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -819,8 +815,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a begin/end to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -848,8 +842,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a ||= to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] From 0456bd9a240f44e36bcdcb64e151e73f21ef60b9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 12:39:04 -0400 Subject: [PATCH 287/524] Support 'foo = nilable || not_nilable' --- lib/solargraph/complex_type.rb | 5 +++++ lib/solargraph/source/chain/or.rb | 7 ++++++- spec/source/chain/or_spec.rb | 2 -- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 669a66900..408b896e5 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -237,6 +237,11 @@ def nullable? @items.any?(&:nil_type?) end + # @return [ComplexType] + def without_nil + ComplexType.new(@items.reject(&:nil_type?)) + end + # @return [Array] def all_params @items.first.all_params || [] diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 1e3a70f40..fd56b52c0 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -15,7 +15,12 @@ def initialize links def resolve api_map, name_pin, locals types = @links.map { |link| link.infer(api_map, name_pin, locals) } - [Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.new(types.uniq), source: :chain)] + combined_type = Solargraph::ComplexType.new(types) + unless types.all?(&:nullable?) + combined_type = combined_type.without_nil + end + + [Solargraph::Pin::ProxyType.anonymous(combined_type, source: :chain)] end end end diff --git a/spec/source/chain/or_spec.rb b/spec/source/chain/or_spec.rb index 85019a0b7..78bddf34d 100644 --- a/spec/source/chain/or_spec.rb +++ b/spec/source/chain/or_spec.rb @@ -17,8 +17,6 @@ def foo a end it 'removes nil from more complex cases' do - pending 'flow-sensitive typing improvements' - source = Solargraph::Source.load_string(%( def foo out = ENV['BAR'] || From 9c6c1c143abe0ee51a3804cd450e9cf4f328a884 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 13:05:46 -0400 Subject: [PATCH 288/524] Drop 'pending's which are already working This would have failed specs except there was already another 'pending' later. Wonder why RSpec doesn't figure this out itself and complain... --- spec/parser/flow_sensitive_typing_spec.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index c5f443d8e..a4f4c330e 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -528,8 +528,6 @@ def verify_repro(repr, throw_the_dice) end it 'uses varname and && in a simple if() - varname second' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( # @param repr [Integer, nil] # @param throw_the_dice [Boolean] @@ -674,8 +672,6 @@ def bar(arr, baz: nil) end it 'uses .nil? in a return if() in an unless to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -751,8 +747,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a switch/case/else to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -784,8 +778,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a ternary operator to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] @@ -869,8 +861,6 @@ def bar(baz: nil) end it 'uses .nil? in a return if() in a try / rescue / ensure to refine types using nil checks' do - pending 'lib/solargraph/parser/flow_sensitive_typing.rb#parse_nilp' - source = Solargraph::Source.load_string(%( class Foo # @param baz [::Boolean, nil] From 10e7306008a2bf1db6217c773e376ad09b3d9830 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 13:24:20 -0400 Subject: [PATCH 289/524] Handle 'foo = nilable_local || non_nilable' scenarios --- lib/solargraph/parser/parser_gem/node_methods.rb | 3 --- spec/parser/node_methods_spec.rb | 4 ---- spec/source/chain/or_spec.rb | 2 -- spec/type_checker/levels/alpha_spec.rb | 2 -- 4 files changed, 11 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 02f790c00..aa1ab4692 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -302,7 +302,6 @@ def repaired_find_recipient_node cursor module DeepInference class << self CONDITIONAL_ALL_BUT_FIRST = [:if, :unless] - CONDITIONAL_ALL = [:or] ONLY_ONE_CHILD = [:return] FIRST_TWO_CHILDREN = [:rescue] COMPOUND_STATEMENTS = [:begin, :kwbegin] @@ -349,8 +348,6 @@ def from_value_position_statement node, include_explicit_returns: true elsif CONDITIONAL_ALL_BUT_FIRST.include?(node.type) result.concat reduce_to_value_nodes(node.children[1..-1]) # result.push NIL_NODE unless node.children[2] - elsif CONDITIONAL_ALL.include?(node.type) - result.concat reduce_to_value_nodes(node.children) elsif ONLY_ONE_CHILD.include?(node.type) result.concat reduce_to_value_nodes([node.children[0]]) elsif FIRST_TWO_CHILDREN.include?(node.type) diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index e5cd0c78f..675f15ce4 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -185,8 +185,6 @@ end it "handles top 'or' nodes" do - pending 'flow-sensitive typing improvements' - node = Solargraph::Parser.parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.length).to eq(1) @@ -336,8 +334,6 @@ end it "handles top 'or' nodes" do - pending 'flow-sensitive typing improvements' - node = Solargraph::Parser.parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:or]) diff --git a/spec/source/chain/or_spec.rb b/spec/source/chain/or_spec.rb index 78bddf34d..084738fe3 100644 --- a/spec/source/chain/or_spec.rb +++ b/spec/source/chain/or_spec.rb @@ -1,7 +1,5 @@ describe Solargraph::Source::Chain::Or do it 'handles simple nil-removal' do - pending 'flow-sensitive typing improvements' - source = Solargraph::Source.load_string(%( # @param a [Integer, nil] def foo a diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index d6c58df00..781907d33 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -21,8 +21,6 @@ def bar(baz: nil) end it 'does not falsely enforce nil in return types' do - pending 'flow-sensitive typing improvements' - checker = type_checker(%( # @return [Integer] def foo From 6f5d60a4be1ea8ef9def194c508c87062dad2490 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 12 Oct 2025 16:04:45 -0400 Subject: [PATCH 290/524] Handle 'while foo' --- .../parser/flow_sensitive_typing.rb | 35 +++++++++++++++++++ lib/solargraph/parser/node_processor/base.rb | 5 +++ .../parser_gem/node_processors/while_node.rb | 5 ++- spec/parser/flow_sensitive_typing_spec.rb | 28 +++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 3ded6010a..071a848bd 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -159,6 +159,41 @@ def process_if(if_node, true_ranges = [], false_ranges = []) process_expression(conditional_node, true_ranges, false_ranges) end + # @param while_node [Parser::AST::Node] + # @param true_ranges [Array] + # @param false_ranges [Array] + # + # @return [void] + def process_while(while_node, true_ranges = [], false_ranges = []) + return if while_node.type != :while + + # + # See if we can refine a type based on the result of 'if foo.nil?' + # + # [3] pry(main)> Parser::CurrentRuby.parse("while a; b; c; end") + # => s(:while, + # s(:send, nil, :a), + # s(:begin, + # s(:send, nil, :b), + # s(:send, nil, :c))) + # [4] pry(main)> + conditional_node = while_node.children[0] + # @type [Parser::AST::Node, nil] + do_clause = while_node.children[1] + + unless do_clause.nil? + # + # If the condition is true we can assume things about the do clause + # + before_do_clause_loc = do_clause.location.expression.adjust(begin_pos: -1) + before_do_clause_pos = Position.new(before_do_clause_loc.line, before_do_clause_loc.column) + true_ranges << Range.new(before_do_clause_pos, + get_node_end_position(do_clause)) + end + + process_expression(conditional_node, true_ranges, false_ranges) + end + class << self include Logging end diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index 4d4123d9d..1a79bea65 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -40,6 +40,11 @@ def process private + # @return [Solargraph::Location] + def location + get_node_location(node) + end + # @return [Solargraph::Position] def position Position.new(node.loc.line, node.loc.column) diff --git a/lib/solargraph/parser/parser_gem/node_processors/while_node.rb b/lib/solargraph/parser/parser_gem/node_processors/while_node.rb index c9211448e..4b025121e 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/while_node.rb @@ -8,7 +8,10 @@ class WhileNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - location = get_node_location(node) + FlowSensitiveTyping.new(locals, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_while(node) + # Note - this should not be considered a block, as the # while statement doesn't create a closure - e.g., # variables created inside can be seen from outside as diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index a4f4c330e..cd2e7b390 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -723,6 +723,34 @@ def bar(baz: nil) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end + it 'uses foo in a a while to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param other [::Boolean, nil] + # @return [void] + def bar(baz: nil, other: nil) + baz + while baz do + baz + baz = other + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + it 'uses .nil? in a return if() in an until to refine types using nil checks' do source = Solargraph::Source.load_string(%( class Foo From 82183e4ad4c0072ed84a1bb54c17c6d75716c4bd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 15 Oct 2025 08:35:57 -0400 Subject: [PATCH 291/524] Handle 'foo unless unrelated || repr.nil? --- lib/solargraph/parser/flow_sensitive_typing.rb | 4 ++-- spec/parser/flow_sensitive_typing_spec.rb | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 071a848bd..42143d6a4 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -63,8 +63,8 @@ def process_or(or_node, true_ranges = [], false_ranges = []) # can't assume if an or is true that every single condition is # true, so don't provide true ranges to assert facts on - process_expression(lhs, [], [rhs_presence]) - process_expression(rhs, [], []) + process_expression(lhs, [], false_ranges + [rhs_presence]) + process_expression(rhs, [], false_ranges) end # @param node [Parser::AST::Node] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index cd2e7b390..50b356f40 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -404,7 +404,7 @@ def verify_repro(repr, throw_the_dice) expect(clip.infer.rooted_tags).to eq('::Integer, nil') clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.rooted_tags).to eq('::Integer, nil') + expect(clip.infer.rooted_tags).to eq('::Integer') end it 'uses nil? and || in a simple if() - nil? second' do @@ -428,7 +428,7 @@ def verify_repro(repr, throw_the_dice) expect(clip.infer.rooted_tags).to eq('::Integer, nil') clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.rooted_tags).to eq('::Integer, nil') + expect(clip.infer.rooted_tags).to eq('::Integer') end it 'uses varname and || in a simple if() - varname first' do @@ -451,8 +451,6 @@ def verify_repro(repr, throw_the_dice) clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - pending('supporting else after || on varname') - clip = api_map.clip_at('test.rb', [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -477,8 +475,6 @@ def verify_repro(repr, throw_the_dice) clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - pending('supporting else after || on varname') - clip = api_map.clip_at('test.rb', [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -497,7 +493,7 @@ def verify_repro(repr) expect(clip.infer.rooted_tags).to eq('::String') clip = api_map.clip_at('test.rb', [4, 8]) - expect(clip.infer.rooted_tags).to eq('::String, nil') + expect(clip.infer.rooted_tags).to eq('::String') clip = api_map.clip_at('test.rb', [5, 8]) expect(clip.infer.rooted_tags).to eq('::String, nil') From 1a4ef7876f338b346187f12fd608f388f90e9c33 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 21 Oct 2025 23:04:41 -0400 Subject: [PATCH 292/524] Use presence information to improve local variable type probing * Use Pin::LocalVariable#combine_with to merge information from multiple assignments to the same local variable that may be visible from a given location. * Offer a single pin with consolidated information with ApiMap#var_at_location * fix: Ensure Pin::Base#reset_generated! gets called consistently when information is invalidated. * fix: Ensure #reduce_class_type is in both ComplexType and UniqueType * fix: Track location information more consistently so that we can use it to determine visibility of assignments. * fix: Handle a number of Pin::Block rebinding cases more consistently so that we can rely on the closure information inside to determine visibility of assignments. * Improve some Pin::Base#inspect representations for easier debugging. --- .rubocop_todo.yml | 13 +- lib/solargraph/api_map.rb | 24 +- lib/solargraph/api_map/index.rb | 12 +- lib/solargraph/complex_type/unique_type.rb | 11 + lib/solargraph/location.rb | 8 +- .../parser/flow_sensitive_typing.rb | 40 - .../parser/parser_gem/class_methods.rb | 15 +- .../parser/parser_gem/node_chainer.rb | 7 +- .../parser_gem/node_processors/block_node.rb | 21 +- .../parser_gem/node_processors/def_node.rb | 6 + lib/solargraph/pin/base.rb | 4 +- lib/solargraph/pin/base_variable.rb | 114 ++- lib/solargraph/pin/block.rb | 17 +- lib/solargraph/pin/local_variable.rb | 101 ++- lib/solargraph/pin/method.rb | 5 + lib/solargraph/pin/parameter.rb | 19 +- lib/solargraph/source.rb | 4 +- lib/solargraph/source/chain.rb | 3 +- lib/solargraph/source/chain/call.rb | 25 +- lib/solargraph/source/source_chainer.rb | 7 +- lib/solargraph/source_map.rb | 2 +- lib/solargraph/source_map/clip.rb | 2 +- lib/solargraph/type_checker.rb | 4 +- spec/parser/flow_sensitive_typing_spec.rb | 806 +++++++++++++++++- spec/parser/node_chainer_spec.rb | 26 +- spec/parser/node_methods_spec.rb | 102 +-- spec/parser/node_processor_spec.rb | 12 +- spec/parser_spec.rb | 8 +- spec/pin/local_variable_spec.rb | 159 ++++ spec/source/chain/call_spec.rb | 31 +- spec/source/chain_spec.rb | 3 +- spec/source_map/clip_spec.rb | 6 +- spec/source_map_spec.rb | 16 +- spec/type_checker/levels/strong_spec.rb | 375 +++++++- 34 files changed, 1751 insertions(+), 257 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83339e756..c68bb6c0a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -458,7 +457,7 @@ Metrics/AbcSize: # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 54 + Max: 56 # Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: @@ -469,6 +468,7 @@ Metrics/ClassLength: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/type_checker.rb' @@ -773,7 +773,6 @@ Style/AndOr: # RedundantBlockArgumentNames: blk, block, proc Style/ArgumentsForwarding: Exclude: - - 'lib/solargraph/api_map.rb' - 'lib/solargraph/complex_type.rb' # This cop supports safe autocorrection (--autocorrect). @@ -1188,7 +1187,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' @@ -1230,7 +1228,12 @@ Style/TrailingCommaInArrayLiteral: # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInHashLiteral: - Enabled: false + Exclude: + - 'lib/solargraph/pin/base_variable.rb' + - 'lib/solargraph/pin/callable.rb' + - 'lib/solargraph/pin/closure.rb' + - 'lib/solargraph/pin/local_variable.rb' + - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 71855d04a..eaa64b756 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -346,10 +346,26 @@ def get_instance_variable_pins(namespace, scope = :instance) result end - # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins - # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins - def visible_pins(*args, **kwargs, &blk) - Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk) + # Find a variable pin by name and where it is used. + # + # Resolves our most specific view of this variable's type by + # preferring pins created by flow-sensitive typing when we have + # them based on the Closure and Location. + # + # @param locals [Array] + # @param name [String] + # @param closure [Pin::Closure] + # @param location [Location] + # + # @return [Pin::LocalVariable, nil] + def var_at_location(locals, name, closure, location) + with_correct_name = locals.select { |pin| pin.name == name} + with_presence = with_correct_name.reject { |pin| pin.presence.nil? } + vars_at_location = with_presence.reject do |pin| + (!pin.visible_at?(closure, location) && + !pin.starts_at?(location)) + end + vars_at_location.inject(&:combine_with) end # Get an array of class variable pins for a namespace. diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 35d86446a..d5f5f1821 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -140,10 +140,13 @@ def map_overrides 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 + pin.reset_generated! + + next unless new_pin + + new_pin.docstring.add_tag(tag) + redefine_return_type new_pin, tag + new_pin.reset_generated! end end end @@ -160,7 +163,6 @@ def redefine_return_type pin, tag 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/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 05a585dcf..6b941d76d 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -437,6 +437,17 @@ def self_to_type dst end end + # @return [ComplexType] + def reduce_class_type + new_items = items.flat_map do |type| + next type unless ['Module', 'Class'].include?(type.name) + next type if type.all_params.empty? + + type.all_params + end + ComplexType.new(new_items) + end + def all_rooted? return true if name == GENERIC_TAG_NAME rooted? && all_params.all?(&:rooted?) diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 713b4fef1..592da03bf 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -13,9 +13,11 @@ class Location # @return [Solargraph::Range] attr_reader :range - # @param filename [String] + # @param filename [String, nil] # @param range [Solargraph::Range] def initialize filename, range + raise "Use nil to represent no-file" if filename&.empty? + @filename = filename @range = range end @@ -64,8 +66,10 @@ def to_hash # @return [Location, nil] def self.from_node(node) return nil if node.nil? || node.loc.nil? + filename = node.loc.expression.source_buffer.name + filename = nil if filename.empty? range = Range.from_node(node) - self.new(node.loc.expression.source_buffer.name, range) + self.new(filename, range) end # @param other [BasicObject] diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 41ce6eeaf..d589488da 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -75,46 +75,6 @@ class << self include Logging end - # Find a variable pin by name and where it is used. - # - # Resolves our most specific view of this variable's type by - # preferring pins created by flow-sensitive typing when we have - # them based on the Closure and Location. - # - # @param pins [Array] - # @param name [String] - # @param closure [Pin::Closure] - # @param location [Location] - # - # @return [Array] - def self.visible_pins(pins, name, closure, location) - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location})" } - pins_with_name = pins.select { |p| p.name == name } - if pins_with_name.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => [] - no pins with name" } - return [] - end - pins_with_specific_visibility = pins.select { |p| p.name == name && p.presence && p.visible_at?(closure, location) } - if pins_with_specific_visibility.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_name} - no pins with specific visibility" } - return pins_with_name - end - visible_pins_specific_to_this_closure = pins_with_specific_visibility.select { |p| p.closure == closure } - if visible_pins_specific_to_this_closure.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_specific_visibility} - no visible pins specific to this closure (#{closure})}" } - return pins_with_specific_visibility - end - flow_defined_pins = pins_with_specific_visibility.select { |p| p.presence_certain? } - if flow_defined_pins.empty? - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{visible_pins_specific_to_this_closure} - no flow-defined pins" } - return visible_pins_specific_to_this_closure - end - - logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{flow_defined_pins}" } - - flow_defined_pins - end - include Logging private diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 2daf22fc7..adafbae32 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -8,19 +8,22 @@ module ParserGem module ClassMethods # @param code [String] # @param filename [String, nil] + # @param starting_line [Integer] must be provided so that we + # can find relevant local variables later even if this is just + # a subset of the file in question # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})] - def parse_with_comments code, filename = nil - node = parse(code, filename) + def parse_with_comments code, filename, starting_line + node = parse(code, filename, starting_line) comments = CommentRipper.new(code, filename, 0).parse [node, comments] end # @param code [String] - # @param filename [String, nil] - # @param line [Integer] + # @param filename [String] + # @param starting_line [Integer] # @return [Parser::AST::Node] - def parse code, filename = nil, line = 0 - buffer = ::Parser::Source::Buffer.new(filename, line) + def parse code, filename, starting_line + buffer = ::Parser::Source::Buffer.new(filename, starting_line) buffer.source = code parser.parse(buffer) rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index d8d46319b..438d99811 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -35,9 +35,12 @@ def chain node, filename = nil, parent = nil end # @param code [String] + # @param filename [String] + # @param starting_line [Integer] + # # @return [Source::Chain] - def load_string(code) - node = Parser.parse(code.sub(/\.$/, '')) + def load_string(code, filename, starting_line) + node = Parser.parse(code.sub(/\.$/, ''), filename, starting_line) chain = NodeChainer.new(node).chain chain.links.push(Chain::Link.new) if code.end_with?('.') chain diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index d773e8e50..c77713fad 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -9,23 +9,22 @@ class BlockNode < Parser::NodeProcessor::Base def process location = get_node_location(node) - parent = if other_class_eval? - Solargraph::Pin::Namespace.new( - location: location, - type: :class, - name: unpack_name(node.children[0].children[0]), - source: :parser, - ) - else - region.closure + binder = nil + scope = region.scope || region.closure.context.scope + if other_class_eval? + clazz_name = unpack_name(node.children[0].children[0]) + binder = ComplexType.try_parse("Class<#{clazz_name}>") + scope = :class end block_pin = Solargraph::Pin::Block.new( location: location, - closure: parent, + closure: region.closure, node: node, + context: binder, + binder: binder, receiver: node.children[0], comments: comments_for(node), - scope: region.scope || region.closure.context.scope, + scope: scope, source: :parser ) pins.push block_pin diff --git a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb index 47c01e728..5d0d9a284 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -12,6 +12,8 @@ def process location: get_node_location(node), closure: region.closure, name: name, + context: region.closure.binder.namespace_type, + binder: region.closure.binder.namespace_type, comments: comments_for(node), scope: scope, visibility: scope == :instance && name == 'initialize' ? :private : region.visibility, @@ -23,6 +25,8 @@ def process location: methpin.location, closure: methpin.closure, name: methpin.name, + context: region.closure.binder, + binder: region.closure.binder, comments: methpin.comments, scope: :class, visibility: :public, @@ -34,6 +38,8 @@ def process location: methpin.location, closure: methpin.closure, name: methpin.name, + context: region.closure.binder, + binder: region.closure.binder, comments: methpin.comments, scope: :instance, visibility: :private, diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 511c7deb7..d56f0fffd 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -81,7 +81,6 @@ def closure # # @return [self] def combine_with(other, attrs={}) - raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class priority_choice = choose_priority(other) return priority_choice unless priority_choice.nil? @@ -572,6 +571,7 @@ def proxy return_type result = dup result.return_type = return_type result.proxied = true + result.reset_generated! result end @@ -616,7 +616,7 @@ def type_desc # @return [String] def inner_desc - closure_info = closure&.desc + closure_info = closure&.name.inspect binder_info = binder&.desc "name=#{name.inspect} return_type=#{type_desc}, context=#{context.rooted_tags}, closure=#{closure_info}, binder=#{binder_info}" end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 764c1fb39..af1d44c9a 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -6,28 +6,52 @@ class BaseVariable < Base # include Solargraph::Source::NodeMethods include Solargraph::Parser::NodeMethods - # @return [Parser::AST::Node, nil] - attr_reader :assignment + # @return [Array] + attr_reader :assignments attr_accessor :mass_assignment # @param return_type [ComplexType, nil] - # @param assignment [Parser::AST::Node, nil] - def initialize assignment: nil, return_type: nil, **splat + # @param assignment [Parser::AST::Node, nil] First assignment + # that was made to this variable + # @param assignments [Array] Possible + # assignments that may have been made to this variable + # @param mass_assignment [Array(Parser::AST::Node, Integer), nil] + def initialize assignment: nil, assignments: [], mass_assignment: nil, return_type: nil, **splat super(**splat) - @assignment = assignment + @assignments = (assignment.nil? ? [] : [assignment]) + assignments # @type [nil, ::Array(Parser::AST::Node, Integer)] - @mass_assignment = nil + @mass_assignment = mass_assignment @return_type = return_type end + def reset_generated! + @assignment = nil + super + end + def combine_with(other, attrs={}) - attrs.merge({ - assignment: assert_same(other, :assignment), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + new_assignments = combine_assignments(other) + new_attrs = attrs.merge({ + assignments: new_assignments, mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), - }) - super(other, attrs) + }) + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + super(other, new_attrs) + end + + # @return [Parser::AST::Node, nil] + def assignment + @assignment ||= assignments.last + end + + # @param other [self] + # + # @return [::Array] + def combine_assignments(other) + (other.assignments + assignments).uniq end def completion_item_kind @@ -80,11 +104,13 @@ def return_types_from_node(parent_node, api_map) # @param api_map [ApiMap] # @return [ComplexType] def probe api_map - unless @assignment.nil? - types = return_types_from_node(@assignment, api_map) - return ComplexType.new(types.uniq) unless types.empty? - end + assignment_types = assignments.flat_map { |node| return_types_from_node(node, api_map) } + type_from_assignment = ComplexType.new(assignment_types.flat_map(&:items).uniq) unless assignment_types.empty? + return type_from_assignment unless type_from_assignment.nil? + # @todo should handle merging types from mass assignments as + # well so that we can do better flow sensitive typing with + # multiple assignments unless @mass_assignment.nil? mass_node, index = @mass_assignment types = return_types_from_node(mass_node, api_map) @@ -95,7 +121,7 @@ def probe api_map type.all_params.first end end.compact! - return ComplexType.new(types.uniq) unless types.empty? + return ComplexType.new(types.uniq).qualify(api_map, *gates) unless types.empty? end ComplexType::UNDEFINED @@ -113,6 +139,64 @@ def type_desc private + # See if this variable is visible within 'other_closure' + # + # @param other_closure [Pin::Closure] + # @return [Boolean] + def visible_in_closure? other_closure + needle = closure + haystack = other_closure + + cursor = haystack + + until cursor.nil? + if cursor.is_a?(Pin::Method) && closure.context.tags == 'Class<>' + # methods can't see local variables declared in their + # parent closure + return false + end + + if cursor.binder.namespace == needle.binder.namespace + return true + end + + if cursor.return_type == needle.context + return true + end + + if scope == :instance && cursor.is_a?(Pin::Namespace) + # classes and modules can't see local variables declared + # in their parent closure, so stop here + return false + end + + cursor = cursor.closure + end + false + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [ComplexType, nil] + def combine_types(other, attr) + # @type [ComplexType, nil] + type1 = send(attr) + # @type [ComplexType, nil] + type2 = other.send(attr) + if type1 && type2 + types = (type1.items + type2.items).uniq + ComplexType.new(types) + else + type1 || type2 + end + end + + # @return [::Symbol] + def scope + :instance + end + # @return [ComplexType] def generate_complex_type tag = docstring.tag(:type) diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 227bc0873..7a63ccc13 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -12,14 +12,16 @@ class Block < Callable attr_reader :node # @param receiver [Parser::AST::Node, nil] + # @param binder [Pin::Namespace, nil] # @param node [Parser::AST::Node, nil] # @param context [ComplexType, nil] # @param args [::Array] - def initialize receiver: nil, args: [], context: nil, node: nil, **splat + def initialize receiver: nil, binder: nil, args: [], context: nil, node: nil, **splat super(**splat, parameters: args) @receiver = receiver @context = context @return_type = ComplexType.parse('::Proc') + @rebind = binder if binder @node = node end @@ -85,7 +87,7 @@ def typify_parameters(api_map) def maybe_rebind api_map return ComplexType::UNDEFINED unless receiver - chain = Parser.chain(receiver, location.filename) + chain = Parser.chain(receiver, location.filename, node) 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 @@ -93,8 +95,15 @@ def maybe_rebind api_map types = receiver_pin.docstring.tag(:yieldreceiver)&.types return ComplexType::UNDEFINED unless types&.any? - target = chain.base.infer(api_map, receiver_pin, locals) - target = full_context unless target.defined? + name_pin = self + # if we have Foo.bar { |x| ... }, and the bar method references self... + target = if chain.base.defined? + # figure out Foo + chain.base.infer(api_map, name_pin, locals) + else + # if not, any self there must be the context of our closure + closure.full_context + end ComplexType.try_parse(*types).qualify(api_map, *receiver_pin.gates).self_to_type(target) end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 9eae6cc6f..427122329 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -3,75 +3,104 @@ module Solargraph module Pin class LocalVariable < BaseVariable - # @return [Range] + # @return [Range, nil] attr_reader :presence def presence_certain? @presence_certain end - # @param assignment [AST::Node, nil] # @param presence [Range, nil] # @param presence_certain [Boolean] # @param splat [Hash] - def initialize assignment: nil, presence: nil, presence_certain: false, **splat + def initialize presence: nil, presence_certain: false, **splat super(**splat) - @assignment = assignment @presence = presence @presence_certain = presence_certain end def combine_with(other, attrs={}) - new_attrs = { - assignment: assert_same(other, :assignment), - presence_certain: assert_same(other, :presence_certain?), - }.merge(attrs) - # @sg-ignore Wrong argument type for - # Solargraph::Pin::Base#assert_same: other expected - # Solargraph::Pin::Base, received self - new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) + # keep this as a parameter + return other.combine_with(self, attrs) if other.is_a?(Parameter) && !self.is_a?(Parameter) + + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + new_assignments = combine_assignments(other) + new_attrs = attrs.merge({ + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + presence: combine_presence(other), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + presence_certain: combine_presence_certain(other), + }) super(other, new_attrs) end + def inner_desc + super + ", presence=#{presence.inspect}, presence_certain=#{presence_certain?}" + end + + # @param other_loc [Location] + def starts_at?(other_loc) + location&.filename == other_loc.filename && + presence && + presence.start == other_loc.range.start + end + # @param other_closure [Pin::Closure] # @param other_loc [Location] def visible_at?(other_closure, other_loc) location.filename == other_loc.filename && - presence.include?(other_loc.range.start) && - match_named_closure(other_closure, closure) + (!presence || presence.include?(other_loc.range.start)) && + visible_in_closure?(other_closure) end def to_rbs (name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped') end - private + # @param other [self] + # @return [ComplexType, nil] + def combine_return_type(other) + if presence_certain? && return_type&.defined? + # flow sensitive typing has already figured out this type + # has been downcast - use the type it figured out + return return_type + end + if other.presence_certain? && other.return_type&.defined? + return other.return_type + end + combine_types(other, :return_type) + end + + def probe api_map + if presence_certain? && return_type&.defined? + # flow sensitive typing has already probed this type - use + # the type it figured out + return return_type.qualify(api_map, *gates) + end - # @param tag1 [String] - # @param tag2 [String] - # @return [Boolean] - def match_tags tag1, tag2 - # @todo This is an unfortunate hack made necessary by a discrepancy in - # how tags indicate the root namespace. The long-term solution is to - # standardize it, whether it's `Class<>`, an empty string, or - # something else. - tag1 == tag2 || - (['', 'Class<>'].include?(tag1) && ['', 'Class<>'].include?(tag2)) + super end - # @param needle [Pin::Base] - # @param haystack [Pin::Base] + # Narrow the presence range to the intersection of both. + # + # @param other [self] + # + # @return [Range, nil] + def combine_presence(other) + return presence || other.presence if presence.nil? || other.presence.nil? + + Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min) + end + + # If a certain pin is being combined with an uncertain pin, we + # end up with a certain result + # + # @param other [self] + # # @return [Boolean] - def match_named_closure needle, haystack - return true if needle == haystack || haystack.is_a?(Pin::Block) - cursor = haystack - until cursor.nil? - return true if needle.path == cursor.path - return false if cursor.path && !cursor.path.empty? - cursor = cursor.closure - end - false + def combine_presence_certain(other) + presence_certain? || other.presence_certain? end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 011f096f6..94d254ddc 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -22,7 +22,10 @@ class Method < Callable # @param attribute [Boolean] # @param signatures [::Array, nil] # @param anon_splat [Boolean] + # @param context [ComplexType, nil] + # @param binder [ComplexType, nil] def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, + context: nil, binder: nil, **splat super(**splat) @visibility = visibility @@ -32,6 +35,8 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @attribute = attribute @signatures = signatures @anon_splat = anon_splat + @context = context if context + @binder = binder if binder end # @return [Array] diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 947513689..af02a2f1c 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -30,12 +30,19 @@ def location 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) + # Parameters can be combined with local variables + new_attrs = if other.is_a?(Parameter) + { + decl: assert_same(other, :decl), + asgn_code: choose(other, :asgn_code) + } + else + { + decl: decl, + asgn_code: asgn_code + } + end + super(other, new_attrs.merge(attrs)) end def keyword? diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index ae5b08d3b..1bbfc1991 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -396,7 +396,7 @@ def finalize end @finalized = true begin - @node, @comments = Solargraph::Parser.parse_with_comments(@code, filename) + @node, @comments = Solargraph::Parser.parse_with_comments(@code, filename, 0) @parsed = true @repaired = @code rescue Parser::SyntaxError, EncodingError => e @@ -412,7 +412,7 @@ def finalize end error_ranges.concat(changes.map(&:range)) begin - @node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename) + @node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename, 0) @parsed = true rescue Parser::SyntaxError, EncodingError => e @node = nil diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index c08d04878..e941a347d 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -138,7 +138,8 @@ def define api_map, name_pin, locals # @return [ComplexType] # @sg-ignore def infer api_map, name_pin, locals - cache_key = [node, node&.location, links, name_pin&.return_type, locals] + # includes binder as it is mutable in Pin::Block + cache_key = [node, node&.location, links, name_pin&.return_type, name_pin&.binder, locals] if @@inference_invalidation_key == api_map.hash cached = @@inference_cache[cache_key] return cached if cached diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 24d10656d..08193afc3 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -50,12 +50,8 @@ def with_block? def resolve api_map, name_pin, locals return super_pins(api_map, name_pin) if word == 'super' return yield_pins(api_map, name_pin) if word == 'yield' - found = if head? - api_map.visible_pins(locals, word, name_pin, location) - else - [] - end - return inferred_pins(found, api_map, name_pin, locals) unless found.empty? + found = api_map.var_at_location(locals, word, name_pin, location) if head? + return inferred_pins([found], api_map, name_pin, locals) unless found.nil? pins = name_pin.binder.each_unique_type.flat_map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag stack = api_map.get_method_stack(ns_tag, word, scope: context.scope) @@ -67,7 +63,7 @@ def resolve api_map, name_pin, locals private - # @param pins [::Enumerable] + # @param pins [::Enumerable] # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @param locals [::Array] @@ -98,7 +94,10 @@ def inferred_pins pins, api_map, name_pin, locals match = ol.parameters.any?(&:restarg?) break end - atype = atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(name_pin.context, source: :chain), locals) + arg_name_pin = Pin::ProxyType.anonymous(name_pin.context, + closure: name_pin.closure, + source: :chain) + atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals) unless param.compatible_arg?(atype, api_map) || param.restarg? match = false break @@ -309,7 +308,7 @@ def block_symbol_call_type(api_map, context, block_parameter_types, locals) # @return [Pin::Block, nil] def find_block_pin(api_map) node_location = Solargraph::Location.from_node(block.node) - return if node_location.nil? + return if node_location.nil? block_pins = api_map.get_block_pins block_pins.find { |pin| pin.location.contain?(node_location) } end @@ -322,10 +321,12 @@ def find_block_pin(api_map) def block_call_type(api_map, name_pin, locals) return nil unless with_block? - block_context_pin = name_pin block_pin = find_block_pin(api_map) - block_context_pin = block_pin.closure if block_pin - block.infer(api_map, block_context_pin, locals) + # We use the block pin as the closure, as the parameters + # here will only be defined inside the block itself and we need to be able to see them + + # @sg-ignore Need to add nil check here + block.infer(api_map, block_pin, locals) end end end diff --git a/lib/solargraph/source/source_chainer.rb b/lib/solargraph/source/source_chainer.rb index 5758a9d35..94dfb1a4d 100644 --- a/lib/solargraph/source/source_chainer.rb +++ b/lib/solargraph/source/source_chainer.rb @@ -44,14 +44,15 @@ def chain node, parent = tree[0..2] elsif source.parsed? && source.repaired? && end_of_phrase == '.' node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] - node = Parser.parse(fixed_phrase) if node.nil? + # provide filename and line so that we can look up local variables there later + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil? elsif source.repaired? - node = Parser.parse(fixed_phrase) + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) else node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] unless source.error_ranges.any?{|r| r.nil? || r.include?(fixed_position)} # Exception for positions that chain literal nodes in unsynchronized sources node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil? - node = Parser.parse(fixed_phrase) if node.nil? + node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil? end rescue Parser::SyntaxError return Chain.new([Chain::UNDEFINED_CALL]) diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index d7b6fb4fc..78fb77dd8 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -141,7 +141,7 @@ def references name # @return [Array] def locals_at(location) return [] if location.filename != filename - closure = locate_named_path_pin(location.range.start.line, location.range.start.character) + closure = locate_closure_pin(location.range.start.line, location.range.start.character) locals.select { |pin| pin.visible_at?(closure, location) } end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 3d198ac1e..f7e76b711 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -78,7 +78,7 @@ def gates # @param phrase [String] # @return [Array] def translate phrase - chain = Parser.chain(Parser.parse(phrase)) + chain = Parser.chain(Parser.parse(phrase, cursor.filename, cursor.position.line)) chain.define(api_map, closure, locals) end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 4600767b5..2486e01d8 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -745,8 +745,8 @@ def fake_args_for(pin) args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)]) end end - args.push Solargraph::Parser.chain_string('{}') if with_opts - args.push Solargraph::Parser.chain_string('&') if with_block + args.push Solargraph::Parser.chain_string('{}', filename, pin.position.line) if with_opts + args.push Solargraph::Parser.chain_string('&', filename, pin.position.line) if with_block args end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index bf747fc76..460571cb6 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -3,7 +3,7 @@ # @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 + it 'uses is_a? in a simple if() to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -24,6 +24,30 @@ def verify_repro(repr) expect(clip.infer.to_s).to eq('ReproBase') end + it 'uses is_a? in a simple if() with a union to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro1 < ReproBase; end + class Repro2 < ReproBase; end + # @param repr [Repro1, Repro2] + def verify_repro(repr) + if repr.is_a?(Repro1) + repr + else + repr + end + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + 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') + 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 @@ -72,7 +96,7 @@ def verify_repro(repr) 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 + it 'uses is_a? in a simple unless statement to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end class Repro < ReproBase; end @@ -173,22 +197,6 @@ class Repro < ReproBase; end 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] @@ -212,6 +220,68 @@ class Repro < ReproBase; end expect(clip.infer.to_s).to eq('Float') end + it 'uses varname in a simple if()' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname 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, nil] + value = bar + while !is_done() + break unless value + value + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('ReproBase') + end + + it 'uses varname in a "break if" statement in a while to refine types' do + source = Solargraph::Source.load_string(%( + class ReproBase; end + class Repro < ReproBase; end + # @type [ReproBase, nil] + value = bar + while !is_done() + break if value.nil? + value + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 8]) + expect(clip.infer.to_s).to eq('ReproBase') + end + it 'understands compatible reassignments' do source = Solargraph::Source.load_string(%( class Foo @@ -253,4 +323,704 @@ def baz; end clip = api_map.clip_at('test.rb', [3, 6]) expect { clip.infer.to_s }.not_to raise_error end + + it 'uses nil? in a simple if() to refine nilness' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and && in a simple if() to refine nilness - nil? second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses nil? and || in a simple if() - nil? first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr.nil? || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses nil? and || in a simple if() - nil? second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr.nil? + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + end + + it 'uses varname and || in a simple if() - varname first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr || throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses varname and || in a simple if() - varname second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice || repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses .nil? and or in an unless' do + source = Solargraph::Source.load_string(%( + # @param repr [String, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr) + repr unless repr.nil? || repr.downcase + repr + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 33]) + expect(clip.infer.rooted_tags).to eq('::String') + + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::String') + + clip = api_map.clip_at('test.rb', [5, 8]) + expect(clip.infer.rooted_tags).to eq('::String, nil') + end + + it 'uses varname and && in a simple if() - varname first' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if repr && throw_the_dice + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses varname and && in a simple if() - varname second' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + # @param throw_the_dice [Boolean] + def verify_repro(repr, throw_the_dice) + repr + if throw_the_dice && repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + end + + it 'uses variable in a simple if() to refine types' do + source = Solargraph::Source.load_string(%( + # @param repr [Integer, nil] + def verify_repro(repr) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [4, 8]) + expect(clip.infer.rooted_tags).to eq('::Integer, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Integer') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses variable in a simple if() to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + def verify_repro(repr = nil) + repr = 10 if floop + repr + if repr + repr + else + repr + end + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [3, 8]) + expect(clip.infer.rooted_tags).to eq('10, nil') + + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('10') + + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('nil') + end + + it 'uses .nil? in a return if() in an if to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + if rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + # https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf + # https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html + it 'uses .nil? in a return if() in a method to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + return if baz.nil? + baz + end + end + ), 'test.rb') + + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a block to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param arr [Array] + # @return [void] + def bar(arr, baz: nil) + baz + arr.each do |item| + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [9, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an unless to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + unless rand + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in unless') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a while to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + while rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses foo in a a while to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @param other [::Boolean, nil] + # @return [void] + def bar(baz: nil, other: nil) + baz + while baz do + baz + baz = other + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [6, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [11, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in an until to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + until rand do + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [7, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [9, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a switch/case/else to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + case rand + when 0..0.5 + return if baz.nil? + baz + else + baz + end + baz + end + end + ), 'test.rb') + + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in case/when') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [12, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a ternary operator to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + rand > 0.5 ? (return if baz.nil?; baz) : baz + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [6, 44]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in ternary operator') + + clip = api_map.clip_at('test.rb', [6, 51]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [7, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'uses .nil? in a return if() in a begin/end to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a ||= to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + baz ||= begin + return if baz.nil? + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + end + + it 'uses .nil? in a return if() in a try / rescue / ensure to refine types using nil checks' do + source = Solargraph::Source.load_string(%( + class Foo + # @param baz [::Boolean, nil] + # @return [void] + def bar(baz: nil) + baz + begin + return if baz.nil? + baz + rescue StandardError + baz + ensure + baz + end + baz + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [8, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + clip = api_map.clip_at('test.rb', [10, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean') + + pending('better scoping of return if in begin/rescue/ensure') + + clip = api_map.clip_at('test.rb', [12, 12]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + + clip = api_map.clip_at('test.rb', [14, 10]) + expect(clip.infer.rooted_tags).to eq('::Boolean, nil') + end + + it 'provides a useful pin after a return if .nil?' do + source = Solargraph::Source.load_string(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + c + return c if c.nil? + c + 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('String') + + pending 'FlowSensitiveTyping improvements' + + clip = api_map.clip_at('test.rb', [7, 17]) + expect(clip.infer.to_s).to eq('nil') + + clip = api_map.clip_at('test.rb', [8, 10]) + expect(clip.infer.to_s).to eq('String') + end + + it 'uses ! to detect nilness' do + source = Solargraph::Source.load_string(%( + class A + # @param a [Integer, nil] + # @return [Integer] + def foo a + return a unless !a + 123 + end + end + ), 'test.rb') + pending 'FlowSensitiveTyping improvements' + + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [5, 17]) + expect(clip.infer.to_s).to eq('Integer') + end end diff --git a/spec/parser/node_chainer_spec.rb b/spec/parser/node_chainer_spec.rb index e92431aae..e4722d77a 100644 --- a/spec/parser/node_chainer_spec.rb +++ b/spec/parser/node_chainer_spec.rb @@ -1,51 +1,55 @@ describe 'NodeChainer' do + def chain_string str + Solargraph::Parser.chain_string(str, 'file.rb', 0) + end + it "recognizes self keywords" do - chain = Solargraph::Parser.chain_string('self.foo') + chain = chain_string('self.foo') expect(chain.links.first.word).to eq('self') expect(chain.links.first).to be_a(Solargraph::Source::Chain::Head) end it "recognizes super keywords" do - chain = Solargraph::Parser.chain_string('super.foo') + chain = chain_string('super.foo') expect(chain.links.first.word).to eq('super') expect(chain.links.first).to be_a(Solargraph::Source::Chain::ZSuper) end it "recognizes constants" do - chain = Solargraph::Parser.chain_string('Foo::Bar') + chain = chain_string('Foo::Bar') expect(chain.links.length).to eq(1) expect(chain.links.first).to be_a(Solargraph::Source::Chain::Constant) expect(chain.links.map(&:word)).to eq(['Foo::Bar']) end it "splits method calls with arguments and blocks" do - chain = Solargraph::Parser.chain_string('var.meth1(1, 2).meth2 do; end') + chain = chain_string('var.meth1(1, 2).meth2 do; end') expect(chain.links.map(&:word)).to eq(['var', 'meth1', 'meth2']) end it "recognizes literals" do - chain = Solargraph::Parser.chain_string('"string"') + chain = chain_string('"string"') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('100') + chain = chain_string('100') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('[1, 2, 3]') + chain = chain_string('[1, 2, 3]') expect(chain).to be_literal - chain = Solargraph::Parser.chain_string('{ foo: "bar" }') + chain = chain_string('{ foo: "bar" }') expect(chain).to be_literal end it "recognizes instance variables" do - chain = Solargraph::Parser.chain_string('@foo') + chain = chain_string('@foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::InstanceVariable) end it "recognizes class variables" do - chain = Solargraph::Parser.chain_string('@@foo') + chain = chain_string('@@foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::ClassVariable) end it "recognizes global variables" do - chain = Solargraph::Parser.chain_string('$foo') + chain = chain_string('$foo') expect(chain.links.first).to be_a(Solargraph::Source::Chain::GlobalVariable) end diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index f9504b584..561195a40 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -1,65 +1,69 @@ # These tests are deliberately generic because they apply to both the Legacy # and Rubyvm node methods. describe Solargraph::Parser::NodeMethods do + def parse source + Solargraph::Parser.parse(source, 'test.rb', 0) + end + it "unpacks constant nodes into strings" do - ast = Solargraph::Parser.parse("Foo::Bar") + ast = parse("Foo::Bar") expect(Solargraph::Parser::NodeMethods.unpack_name(ast)).to eq "Foo::Bar" end it "infers literal strings" do - ast = Solargraph::Parser.parse("x = 'string'") + ast = parse("x = 'string'") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::String' end it "infers literal hashes" do - ast = Solargraph::Parser.parse("x = {}") + ast = parse("x = {}") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Hash' end it "infers literal arrays" do - ast = Solargraph::Parser.parse("x = []") + ast = parse("x = []") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Array' end it "infers literal integers" do - ast = Solargraph::Parser.parse("x = 100") + ast = parse("x = 100") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Integer' end it "infers literal floats" do - ast = Solargraph::Parser.parse("x = 10.1") + ast = parse("x = 10.1") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast.children[1])).to eq '::Float' end it "infers literal symbols" do - ast = Solargraph::Parser.parse(":symbol") + ast = parse(":symbol") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' end it "infers double quoted symbols" do - ast = Solargraph::Parser.parse(':"symbol"') + ast = parse(':"symbol"') expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' end it "infers interpolated double quoted symbols" do - ast = Solargraph::Parser.parse(':"#{Object}"') + ast = parse(':"#{Object}"') expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' end it "infers single quoted symbols" do - ast = Solargraph::Parser.parse(":'symbol'") + ast = parse(":'symbol'") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(ast)).to eq '::Symbol' end it 'infers literal booleans' do - true_ast = Solargraph::Parser.parse("true") + true_ast = parse("true") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(true_ast)).to eq '::Boolean' - false_ast = Solargraph::Parser.parse("false") + false_ast = parse("false") expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(false_ast)).to eq '::Boolean' end it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + node = parse(%( return if true )) rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) @@ -69,7 +73,7 @@ end it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + node = parse(%( return bla if true )) rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) @@ -78,7 +82,7 @@ end it 'handles return nodes from case statements' do - node = Solargraph::Parser.parse(%( + node = parse(%( case x when 100 true @@ -90,7 +94,7 @@ end it 'handles return nodes from case statements with else' do - node = Solargraph::Parser.parse(%( + node = parse(%( case x when 100, 125 true @@ -114,7 +118,7 @@ end it 'handles return nodes from case statements with boolean conditions' do - node = Solargraph::Parser.parse(%( + node = parse(%( case true when x true @@ -128,7 +132,7 @@ it "handles return nodes in reduceable (begin) nodes" do # @todo Temporarily disabled. Result is 3 nodes instead of 2. - # node = Solargraph::Parser.parse(%( + # node = parse(%( # begin # return if true # end @@ -138,7 +142,7 @@ end it "handles return nodes after other nodes" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return x )) @@ -147,7 +151,7 @@ end it "handles return nodes with unreachable code" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return x y @@ -157,7 +161,7 @@ end it "handles conditional returns with following code" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return x if foo y @@ -167,7 +171,7 @@ end it "handles return nodes with reduceable code" do - node = Solargraph::Parser.parse(%( + node = parse(%( return begin x if foo y @@ -178,14 +182,14 @@ end it "handles top 'and' nodes" do - node = Solargraph::Parser.parse('1 && "2"') + node = parse('1 && "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.length).to eq(1) expect(rets[0].type.to_s.downcase).to eq('and') end it "handles top 'or' nodes" do - node = Solargraph::Parser.parse('1 || "2"') + node = parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.length).to eq(2) expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[0])).to eq('::Integer') @@ -193,14 +197,14 @@ end it "handles nested 'and' nodes" do - node = Solargraph::Parser.parse('return 1 && "2"') + node = parse('return 1 && "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.length).to eq(1) expect(rets[0].type.to_s.downcase).to eq('and') end it "handles nested 'or' nodes" do - node = Solargraph::Parser.parse('return 1 || "2"') + node = parse('return 1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.length).to eq(2) expect(Solargraph::Parser::NodeMethods.infer_literal_node_type(rets[0])).to eq('::Integer') @@ -208,7 +212,7 @@ end it 'finds return nodes in blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( array.each do |item| return item if foo end @@ -218,7 +222,7 @@ end it 'finds correct return node line in begin expressions' do - node = Solargraph::Parser.parse(%( + node = parse(%( begin 123 '123' @@ -229,7 +233,7 @@ end it 'returns nested return blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( if foo array.each do |item| return item if foo @@ -242,7 +246,7 @@ end it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + node = parse(%( return if true )) rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) @@ -252,7 +256,7 @@ end it "handles return nodes with implicit nil values" do - node = Solargraph::Parser.parse(%( + node = parse(%( return bla if true )) rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) @@ -261,7 +265,7 @@ it "handles return nodes in reduceable (begin) nodes" do # @todo Temporarily disabled. Result is 3 nodes instead of 2 in legacy. - # node = Solargraph::Parser.parse(%( + # node = parse(%( # begin # return if true # end @@ -271,7 +275,7 @@ end it "handles return nodes after other nodes" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return x )) @@ -280,7 +284,7 @@ end it "handles return nodes with unreachable code" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return x y @@ -290,7 +294,7 @@ end xit "short-circuits return node finding after a raise statement in a begin expressiona" do - node = Solargraph::Parser.parse(%( + node = parse(%( raise "Error" y )) @@ -299,7 +303,7 @@ end it "does not short circuit return node finding after a raise statement in a conditional" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 raise "Error" if foo y @@ -309,7 +313,7 @@ end it "does not short circuit return node finding after a return statement in a conditional" do - node = Solargraph::Parser.parse(%( + node = parse(%( x = 1 return "Error" if foo y @@ -319,7 +323,7 @@ end it "handles return nodes with reduceable code" do - node = Solargraph::Parser.parse(%( + node = parse(%( return begin x if foo y @@ -330,31 +334,31 @@ end it "handles top 'and' nodes" do - node = Solargraph::Parser.parse('1 && "2"') + node = parse('1 && "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:and]) end it "handles top 'or' nodes" do - node = Solargraph::Parser.parse('1 || "2"') + node = parse('1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:int, :str]) end it "handles nested 'and' nodes from return" do - node = Solargraph::Parser.parse('return 1 && "2"') + node = parse('return 1 && "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:and]) end it "handles nested 'or' nodes from return" do - node = Solargraph::Parser.parse('return 1 || "2"') + node = parse('return 1 || "2"') rets = Solargraph::Parser::NodeMethods.returns_from_method_body(node) expect(rets.map(&:type)).to eq([:int, :str]) end it 'finds return nodes in blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( array.each do |item| return item if foo end @@ -365,7 +369,7 @@ end it 'returns nested return blocks' do - node = Solargraph::Parser.parse(%( + node = parse(%( if foo array.each do |item| return item if foo @@ -379,7 +383,7 @@ end it 'handles return nodes from case statements' do - node = Solargraph::Parser.parse(%( + node = parse(%( case 1 when 1 then "" else @@ -391,7 +395,7 @@ end it 'handles return nodes from case statements without else' do - node = Solargraph::Parser.parse(%( + node = parse(%( case 1 when 1 "" @@ -402,7 +406,7 @@ end it 'handles return nodes from case statements with super' do - node = Solargraph::Parser.parse(%( + node = parse(%( case other when Docstring Docstring.new([all, other.all].join("\n"), object) @@ -416,13 +420,13 @@ describe 'convert_hash' do it 'converts literal hash arguments' do - node = Solargraph::Parser.parse('{foo: :bar}') + node = parse('{foo: :bar}') hash = Solargraph::Parser::NodeMethods.convert_hash(node) expect(hash.keys).to eq([:foo]) end it 'ignores call arguments' do - node = Solargraph::Parser.parse('some_call') + node = parse('some_call') hash = Solargraph::Parser::NodeMethods.convert_hash(node) expect(hash).to eq({}) end diff --git a/spec/parser/node_processor_spec.rb b/spec/parser/node_processor_spec.rb index 5b8d7cd40..2033e21ca 100644 --- a/spec/parser/node_processor_spec.rb +++ b/spec/parser/node_processor_spec.rb @@ -1,6 +1,10 @@ describe Solargraph::Parser::NodeProcessor do + def parse source + Solargraph::Parser.parse(source, 'file.rb', 0) + end + it 'ignores bare private_constant calls' do - node = Solargraph::Parser.parse(%( + node = parse(%( class Foo private_constant end @@ -11,7 +15,7 @@ class Foo end it 'orders optional args correctly' do - node = Solargraph::Parser.parse(%( + node = parse(%( def foo(bar = nil, baz = nil); end )) pins, = Solargraph::Parser::NodeProcessor.process(node) @@ -21,7 +25,7 @@ def foo(bar = nil, baz = nil); end end it 'understands +=' do - node = Solargraph::Parser.parse(%( + node = parse(%( detail = '' detail += "foo" detail.strip! @@ -53,7 +57,7 @@ def process Solargraph::Parser::NodeProcessor.register(:def, dummy_processor1) Solargraph::Parser::NodeProcessor.register(:def, dummy_processor2) - node = Solargraph::Parser.parse(%( + node = parse(%( def some_method; end )) pins, = Solargraph::Parser::NodeProcessor.process(node) diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 267f412f4..3c1e3cca0 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -1,11 +1,15 @@ describe Solargraph::Parser do + def parse source + Solargraph::Parser.parse(source, 'file.rb', 0) + end + it "parses nodes" do - node = Solargraph::Parser.parse('class Foo; end', 'test.rb') + node = parse('class Foo; end') expect(Solargraph::Parser.is_ast_node?(node)).to be(true) end it 'raises repairable SyntaxError for unknown encoding errors' do code = "# encoding: utf-\nx = 'y'" - expect { Solargraph::Parser.parse(code) }.to raise_error(Solargraph::Parser::SyntaxError) + expect { parse(code) }.to raise_error(Solargraph::Parser::SyntaxError) end end diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 88075efb9..68ed71ae7 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -50,4 +50,163 @@ class Bar expect { pin1.combine_with(pin2) }.to raise_error(RuntimeError, /Inconsistent :closure name/) end end + + + describe '#visible_at?' do + it 'detects scoped methods in rebound blocks' do + source = Solargraph::Source.load_string(%( + object = MyClass.new + + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + clip = api_map.clip_at('test.rb', [2, 0]) + object_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'object' } + expect(object_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(2, 0, 2, 0)) + expect(object_pin.visible_at?(Solargraph::Pin::ROOT_PIN, location)).to be true + end + + it 'does not allow access to top-level locals from top-level methods' do + map = Solargraph::SourceMap.load_string(%( + x = 'string' + def foo + x + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map map.source + x_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + expect(x_pin).not_to be_nil + foo_pin = api_map.get_path_pins('#foo').first + expect(foo_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 9, 3, 9)) + expect(x_pin.visible_at?(foo_pin, location)).to be false + end + + it 'scopes local variables correctly in class_eval blocks' do + map = Solargraph::SourceMap.load_string(%( + class Foo; end + x = 'y' + Foo.class_eval do + foo = :bar + etc + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map map.source + block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 3 + end + expect(block_pin).not_to be_nil + x_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + expect(x_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(5, 10, 5, 10)) + expect(x_pin.visible_at?(block_pin, location)).to be true + end + + it "understands local lookup in root scope" do + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + # @type [Array] + arr = [] + + + ), "test.rb") + api_map.map source + arr_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'arr' } + expect(arr_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(3, 0, 3, 0)) + expect(arr_pin.visible_at?(Solargraph::Pin::ROOT_PIN, location)).to be true + end + + it 'selects local variables using gated scopes' do + source = Solargraph::Source.load_string(%( + lvar1 = 'lvar1' + module MyModule + lvar2 = 'lvar2' + + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + lvar1_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'lvar1' } + expect(lvar1_pin).not_to be_nil + my_module_pin = api_map.get_namespace_pins('MyModule', 'Class<>').first + expect(my_module_pin).not_to be_nil + location = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(4, 0, 4, 0)) + expect(lvar1_pin.visible_at?(my_module_pin, location)).to be false + + lvar2_pin = api_map.source_map('test.rb').locals.find { |p| p.name == 'lvar2' } + expect(lvar2_pin).not_to be_nil + expect(lvar2_pin.visible_at?(my_module_pin, location)).to be true + end + + it 'is visible within same method' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + x = 1 + puts x + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.source_map('test.rb').locals.first + bar_method = api_map.get_path_pins('Foo#bar').first + expect(bar_method).not_to be_nil + range = Solargraph::Range.from_to(4, 16, 4, 17) + location = Solargraph::Location.new('test.rb', range) + expect(pin.visible_at?(bar_method, location)).to be true + end + + it 'is visible within each block scope inside function' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + x = 1 + [2,3,4].each do |i| + puts x + i + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + x = api_map.source_map('test.rb').locals.find { |p| p.name == 'x' } + bar_method = api_map.get_path_pins('Foo#bar').first + each_block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 4 + end + expect(each_block_pin).not_to be_nil + range = Solargraph::Range.from_to(5, 24, 5, 25) + location = Solargraph::Location.new('test.rb', range) + expect(x.visible_at?(each_block_pin, location)).to be true + end + + it 'sees block parameter inside block' do + source = Solargraph::Source.load_string(%( + class Foo + def bar + [1,2,3].each do |i| + puts i + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + i = api_map.source_map('test.rb').locals.find { |p| p.name == 'i' } + bar_method = api_map.get_path_pins('Foo#bar').first + expect(bar_method).not_to be_nil + each_block_pin = api_map.get_block_pins.find do |b| + b.location.range.start.line == 3 + end + expect(each_block_pin).not_to be_nil + range = Solargraph::Range.from_to(4, 24, 4, 25) + location = Solargraph::Location.new('test.rb', range) + expect(i.visible_at?(each_block_pin, location)).to be true + end + end end diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 8b67a3c66..3dc686605 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -224,7 +224,8 @@ def self.bar type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Set') chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 17)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + block_pin = api_map.source_map('test.rb').pins.find { |p| p.is_a?(Solargraph::Pin::Block) } + type = chain.infer(api_map, block_pin, api_map.source_map('test.rb').locals) expect(type.tag).to eq('Class') chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(7, 9)) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) @@ -406,8 +407,9 @@ def foo(params) api_map = Solargraph::ApiMap.new api_map.map source + foo_pin = api_map.source_map('test.rb').pins.find { |p| p.name == 'foo' } chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 8)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, foo_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer') end @@ -443,8 +445,9 @@ def foo api_map = Solargraph::ApiMap.new api_map.map source + foo_pin = api_map.source_map('test.rb').pins.find { |p| p.name == 'foo' } chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 8)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, foo_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::Array<::String>') end @@ -464,8 +467,12 @@ def c api_map = Solargraph::ApiMap.new api_map.map source + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 4 + end + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.tags).to eq('A::B') end @@ -485,8 +492,12 @@ def c api_map = Solargraph::ApiMap.new api_map.map source + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 4 + end + chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(5, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::A::B') end @@ -512,11 +523,17 @@ def d api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(6, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 5 + end + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) expect(type.rooted_tags).to eq('::A::B').or eq('::A::B, ::A::C').or eq('::A::C, ::A::B') + closure_pin = api_map.source_map('test.rb').pins.find do |p| + p.is_a?(Solargraph::Pin::Block) && p.location.range.start.line == 10 + end chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(11, 14)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, closure_pin, api_map.source_map('test.rb').locals) # valid options here: # * emit type checker warning when adding [B.new] and type whole thing as '::A::B' # * type whole thing as '::A::B, A::C' diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index abc8c2b05..369e302e9 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -428,8 +428,9 @@ def obj(foo); end str = obj.stringify ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) + obj_fn_pin = api_map.get_path_pins('Example.obj').first chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(12, 6)) - type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + type = chain.infer(api_map, obj_fn_pin, api_map.source_map('test.rb').locals) expect(type.to_s).to eq('String') end end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index ee7e4bcfa..c0a60fff0 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1230,7 +1230,7 @@ def one updated = source.synchronize(updater) api_map.map updated clip = api_map.clip_at('test.rb', [2, 8]) - expect(clip.complete.pins.first.path).to start_with('Array#') + expect(clip.complete.pins.first&.path).to start_with('Array#') end it 'selects local variables using gated scopes' do @@ -2007,7 +2007,7 @@ def foo ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [8, 6]) + clip = api_map.clip_at('test.rb', [9, 6]) type = clip.infer expect(type.tags).to eq('Integer') @@ -2712,7 +2712,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(':foo, 123, nil') + expect(clip.infer.to_s).to eq('123, :foo, nil') end it 'expands type with conditional reassignments' do diff --git a/spec/source_map_spec.rb b/spec/source_map_spec.rb index 60d4b523e..5d587e27c 100644 --- a/spec/source_map_spec.rb +++ b/spec/source_map_spec.rb @@ -76,7 +76,7 @@ class Foo expect(pin).to be_a(Solargraph::Pin::Block) end - it 'scopes local variables correctly from root def blocks' do + it 'scopes local variables correctly from root def methods' do map = Solargraph::SourceMap.load_string(%( x = 'string' def foo @@ -88,6 +88,20 @@ def foo expect(locals).to be_empty end + it 'scopes local variables correctly from class methods' do + map = Solargraph::SourceMap.load_string(%( + class Foo + x = 'string' + def foo + x + end + end + ), 'test.rb') + loc = Solargraph::Location.new('test.rb', Solargraph::Range.from_to(4, 11, 3, 11)) + locals = map.locals_at(loc) + expect(locals).to be_empty + end + it 'handles op_asgn case with assertions on' do # set SOLARGRAPH_ASSERTS=onto test this old_asserts = ENV.fetch('SOLARGRAPH_ASSERTS', nil) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 970435dc3..1f0812fe4 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,9 +4,128 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'does not misunderstand types during flow-sensitive typing' do + checker = type_checker(%( + class A + # @param b [Hash{String => String}] + # @return [void] + def a b + c = b["123"] + return if c.nil? + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/nil? pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + return 123 if bar.nil? + bar + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects || overriding nilable types' do + checker = type_checker(%( + # @return [String] + def global_config_path + ENV['SOLARGRAPH_GLOBAL_CONFIG'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'is able to probe type over an assignment' do + checker = type_checker(%( + # @return [String] + def global_config_path + out = 'foo' + out + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility in if/foo pattern' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param bar [Symbol, nil] + # @return [Symbol, Integer] + def foo bar + baz = bar + return baz if baz + 123 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles a flow sensitive typing if correctly' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo a = nil + b = a + if b + b.upcase + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'handles another flow sensitive typing if correctly' do + checker = type_checker(%( + class A + # @param e [String] + # @param f [String] + # @return [void] + def d(e, f:); end + + # @return [void] + def a + c = rand ? nil : "foo" + if c + d(c, f: c) + end + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'respects pin visibility' do + checker = type_checker(%( + class Foo + # Get the namespace's type (Class or Module). + # + # @param baz [Integer, nil] + # @return [Integer, nil] + def foo baz = 123 + return nil if baz.nil? + baz + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'does not complain on array dereference' do checker = type_checker(%( - # @param idx [Integer, nil] an index + # @param idx [Integer] an index # @param arr [Array] an array of integers # # @return [void] @@ -17,6 +136,23 @@ def foo(idx, arr) expect(checker.problems.map(&:message)).to be_empty end + it 'understands local evaluation with ||= removes nil from lhs type' do + checker = type_checker(%( + class Foo + def initialize + @bar = nil + end + + # @return [Integer] + def bar + @bar ||= 123 + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + it 'complains on bad @type assignment' do checker = type_checker(%( # @type [Integer] @@ -97,6 +233,31 @@ def bar expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') end + it 'understands complex use of self' do + pending 'https://github.com/castwide/solargraph/pull/1050' + + checker = type_checker(%( + class A + # @param other [self] + # + # @return [void] + def foo other; end + + # @param other [self] + # + # @return [void] + def bar(other); end + end + + class B < A + def bar(other) + foo(other) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'calls out type issues even when keyword issues are there' do pending('fixes to arg vs param checking algorithm') @@ -256,6 +417,149 @@ def bar &block expect(checker.problems).to be_empty end + it 'does not need fully specified container types' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'treats a parameter type of undefined as not provided' do + checker = type_checker(%( + class Foo + # @param foo [Array] + # @return [void] + def bar foo: []; end + + # @param bing [Array] + # @return [void] + def baz(bing) + bar(foo: bing) + generic_values = [1,2,3].map(&:to_s) + bar(foo: generic_values) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failure with no generic tag' do + checker = type_checker(%( + class Foo + # @param foo [Class] + # @return [void] + def bar foo:; end + + # @param bing [Class>] + # @return [void] + def baz(bing) + bar(foo: bing) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores undefined resolution failures' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures from current Solargraph limitation' do + checker = type_checker(%( + class Foo + # @generic T + # @param klass [Class>] + # @return [Set>] + def pins_by_class klass; [].to_set; end + end + class Bar + # @return [Enumerable] + def block_pins + foo = Foo.new + foo.pins_by_class(Integer) + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'ignores generic resolution failures with only one arg' do + checker = type_checker(%( + # @generic T + # @param path [String] + # @param klass [Class>] + # @return [void] + def code_object_at path, klass = Integer + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on select { is_a? } pattern' do + checker = type_checker(%( + # @param arr [Enumerable} + # @return [Enumerable] + def downcast_arr(arr) + arr.select { |pin| pin.is_a?(Integer) } + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via return value' do + checker = type_checker(%( + # @param bar [Integer] + # @return [Integer, nil] + def foo(bar) + bar + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does not complain on adding nil to types via select' do + checker = type_checker(%( + # @return [Float, nil]} + def bar; rand; end + + # @param arr [Enumerable} + # @return [Integer, nil] + def downcast_arr(arr) + # @type [Object, nil] + foo = arr.select { |pin| pin.is_a?(Integer) && bar }.last + foo + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'inherits param tags from superclass methods' do checker = type_checker(%( class Foo @@ -306,5 +610,74 @@ def run_command )) expect(checker.problems.map(&:message)).to be_empty end + + context 'with class name available in more than one gate' do + let(:checker) do + type_checker(%( + module Foo + module Bar + class Symbol + end + end + end + + module Foo + module Baz + class Quux + # @return [void] + def foo + objects_by_class(Bar::Symbol) + end + + # @generic T + # @param klass [Class>] + # @return [Set>] + def objects_by_class klass + # @type [Set>] + s = Set.new + s + end + end + end + end + )) + end + + it 'resolves class name correctly in generic resolution' do + expect(checker.problems.map(&:message)).to be_empty + end + end + + it 'handles "while foo" flow sensitive typing correctly' do + checker = type_checker(%( + # @param a [String, nil] + # @return [void] + def foo a = nil + b = a + while b + b.upcase + b = nil if rand > 0.5 + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + + it 'does flow sensitive typing even inside a block' do + checker = type_checker(%( + class Quux + # @param foo [String, nil] + # + # @return [void] + def baz(foo) + bar = foo + [].each do + bar.upcase unless bar.nil? + end + end + end)) + + expect(checker.problems.map(&:location).map(&:range).map(&:start)).to be_empty + end end end From 8a8469ec7d9df94159a053bc861b3d21fb1b38cf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 08:05:27 -0400 Subject: [PATCH 293/524] Fix issues from typechecking in CI --- lib/solargraph/location.rb | 1 + lib/solargraph/pin/base.rb | 9 ++++++-- lib/solargraph/pin/base_variable.rb | 14 +++++++++++- lib/solargraph/pin/local_variable.rb | 33 ++++++++++++++++++++++++++++ lib/solargraph/pin/parameter.rb | 1 + lib/solargraph/source/chain/call.rb | 1 - lib/solargraph/type_checker.rb | 4 ++-- spec/pin/local_variable_spec.rb | 22 ------------------- 8 files changed, 57 insertions(+), 28 deletions(-) diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 592da03bf..c59df233d 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -6,6 +6,7 @@ module Solargraph # class Location include Equality + include Comparable # @return [String] attr_reader :filename diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index d56f0fffd..6ff60e2cd 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -91,7 +91,7 @@ def combine_with(other, attrs={}) location: location, type_location: type_location, name: combined_name, - closure: choose_pin_attr_with_same_name(other, :closure), + closure: combine_closure(other), comments: choose_longer(other, :comments), source: :combined, docstring: choose(other, :docstring), @@ -146,6 +146,12 @@ def combine_directives(other) [directives + other.directives].uniq end + # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure(other) + choose_pin_attr_with_same_name(other, :closure) + end + # @param other [self] # @return [String] def combine_name(other) @@ -307,7 +313,6 @@ def assert_same_count(other, attr) # @sg-ignore # @return [undefined] def assert_same(other, attr) - return false if other.nil? val1 = send(attr) val2 = other.send(attr) return val1 if val1 == val2 diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index af1d44c9a..433351c29 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -35,13 +35,25 @@ def combine_with(other, attrs={}) new_assignments = combine_assignments(other) new_attrs = attrs.merge({ assignments: new_assignments, - mass_assignment: assert_same(other, :mass_assignment), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + mass_assignment: combine_mass_assignment(other), return_type: combine_return_type(other), }) # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end + # @param other [self] + # + # @return [Array(Parser::AST::Node, Integer), nil] + # + # @sg-ignore + # Solargraph::Pin::BaseVariable#combine_mass_assignment return + # type could not be inferred + def combine_mass_assignment(other) + assert_same(other, :mass_assignment) + end + # @return [Parser::AST::Node, nil] def assignment @assignment ||= assignments.last diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 427122329..72b85b6d5 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -46,6 +46,30 @@ def starts_at?(other_loc) presence.start == other_loc.range.start end + # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure(other) + return closure if self.closure == other.closure + + # choose first defined, as that establishes the scope of the variable + if closure.nil? || other.closure.nil? + Solargraph.assert_or_log(:varible_closure_missing) do + "One of the local variables being combined is missing a closure: " \ + "#{self.inspect} vs #{other.inspect}" + end + return closure || other.closure + end + + if closure.location.nil? || other.closure.location.nil? + return closure.location.nil? ? other.closure : closure + end + + # if filenames are different, this will just pick one + return closure if closure.location <= other.closure.location + + other.closure + end + # @param other_closure [Pin::Closure] # @param other_loc [Location] def visible_at?(other_closure, other_loc) @@ -72,6 +96,15 @@ def combine_return_type(other) combine_types(other, :return_type) end + # @param other [self] + # + # @return [Array(AST::Node, Integer), nil] + def combine_mass_assignment(other) + # @todo pick first non-nil arbitrarily - we don't yet support + # mass assignment merging + mass_assignment || other.mass_assignment + end + def probe api_map if presence_certain? && return_type&.defined? # flow sensitive typing has already probed this type - use diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index af02a2f1c..4018d1201 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -42,6 +42,7 @@ def combine_with(other, attrs={}) asgn_code: asgn_code } end + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs.merge(attrs)) end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 08193afc3..bd8382851 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -325,7 +325,6 @@ def block_call_type(api_map, name_pin, locals) # We use the block pin as the closure, as the parameters # here will only be defined inside the block itself and we need to be able to see them - # @sg-ignore Need to add nil check here block.infer(api_map, block_pin, locals) end end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 2486e01d8..7e4e910ed 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -745,8 +745,8 @@ def fake_args_for(pin) args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)]) end end - args.push Solargraph::Parser.chain_string('{}', filename, pin.position.line) if with_opts - args.push Solargraph::Parser.chain_string('&', filename, pin.position.line) if with_block + args.push Solargraph::Parser.chain_string('{}', filename, pin.location&.line || 0) if with_opts + args.push Solargraph::Parser.chain_string('&', filename, pin.location&.line || 0) if with_block args end diff --git a/spec/pin/local_variable_spec.rb b/spec/pin/local_variable_spec.rb index 68ed71ae7..97e11db93 100644 --- a/spec/pin/local_variable_spec.rb +++ b/spec/pin/local_variable_spec.rb @@ -30,28 +30,6 @@ class Foo # should indicate which one should override in the range situation end - it "asserts on attempt to merge namespace changes" do - map1 = Solargraph::SourceMap.load_string(%( - class Foo - foo = 'foo' - end - )) - pin1 = map1.locals.first - map2 = Solargraph::SourceMap.load_string(%( - class Bar - foo = 'foo' - end - )) - pin2 = map2.locals.first - # set env variable 'FOO' to 'true' in block - - with_env_var('SOLARGRAPH_ASSERTS', 'on') do - 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 - - describe '#visible_at?' do it 'detects scoped methods in rebound blocks' do source = Solargraph::Source.load_string(%( From c29deaff0e4e21d7c0a80f3841cf5fb22bf62a4b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 08:30:12 -0400 Subject: [PATCH 294/524] Fix type issue --- lib/solargraph/type_checker.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 7e4e910ed..3ce0a9b35 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -745,8 +745,9 @@ def fake_args_for(pin) args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)]) end end - args.push Solargraph::Parser.chain_string('{}', filename, pin.location&.line || 0) if with_opts - args.push Solargraph::Parser.chain_string('&', filename, pin.location&.line || 0) if with_block + starting_line = pin.location&.range&.start&.line || 0 + args.push Solargraph::Parser.chain_string('{}', filename, starting_line) if with_opts + args.push Solargraph::Parser.chain_string('&', filename, starting_line) if with_block args end From 46088728ba70701a8c48333c87e141e7622be617 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 09:22:41 -0400 Subject: [PATCH 295/524] Fix RuboCop issue --- lib/solargraph/type_checker.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 3ce0a9b35..8febd7d54 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -745,7 +745,8 @@ def fake_args_for(pin) args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)]) end end - starting_line = pin.location&.range&.start&.line || 0 + pin_location = pin.location + starting_line = pin_location ? pin_location.range.start.line : 0 args.push Solargraph::Parser.chain_string('{}', filename, starting_line) if with_opts args.push Solargraph::Parser.chain_string('&', filename, starting_line) if with_block args From 350b01ea34c253c099d88806875667cdfb9ef2b8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 09:22:58 -0400 Subject: [PATCH 296/524] Deal with issue seen in typechecking --- lib/solargraph/parser/parser_gem/node_processors/def_node.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb index 5d0d9a284..309de01a1 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -8,12 +8,13 @@ class DefNode < Parser::NodeProcessor::Base def process name = node.children[0].to_s scope = region.scope || (region.closure.is_a?(Pin::Singleton) ? :class : :instance) + method_binder = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder methpin = Solargraph::Pin::Method.new( location: get_node_location(node), closure: region.closure, name: name, - context: region.closure.binder.namespace_type, - binder: region.closure.binder.namespace_type, + context: method_binder, + binder: method_binder, comments: comments_for(node), scope: scope, visibility: scope == :instance && name == 'initialize' ? :private : region.visibility, From 6e0fc44bcfc708a6664c831d9723549f94c69040 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 22:52:30 -0400 Subject: [PATCH 297/524] Set default on parameter for solargraph-rails compatibility --- lib/solargraph/parser/parser_gem/class_methods.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index adafbae32..654ea1b01 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -12,7 +12,7 @@ module ClassMethods # can find relevant local variables later even if this is just # a subset of the file in question # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})] - def parse_with_comments code, filename, starting_line + def parse_with_comments code, filename, starting_line = 0 node = parse(code, filename, starting_line) comments = CommentRipper.new(code, filename, 0).parse [node, comments] From c9abf56918ff7f2a40b838bc0537883c067b76b8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 22 Oct 2025 23:02:21 -0400 Subject: [PATCH 298/524] Set default on parameter for solargraph-rails compatibility --- lib/solargraph/parser/parser_gem/class_methods.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 654ea1b01..1cc40e33b 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -22,7 +22,7 @@ def parse_with_comments code, filename, starting_line = 0 # @param filename [String] # @param starting_line [Integer] # @return [Parser::AST::Node] - def parse code, filename, starting_line + def parse code, filename, starting_line = 0 buffer = ::Parser::Source::Buffer.new(filename, starting_line) buffer.source = code parser.parse(buffer) From 39704a0648106fb10fb05a596977728601a12505 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 23 Oct 2025 08:49:23 -0400 Subject: [PATCH 299/524] Force build --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7f344c712..2382a25f3 100755 --- a/README.md +++ b/README.md @@ -132,9 +132,7 @@ 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. +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. From bba54e748f7f7f567cb4fb409924b151eafc4816 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 17:34:37 -0400 Subject: [PATCH 300/524] Add missing-but-probably-needed method --- lib/solargraph/complex_type/unique_type.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 05a585dcf..946b26374 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -118,6 +118,13 @@ def non_literal_name @non_literal_name ||= determine_non_literal_name end + # @return [self] + def without_nil + return UniqueType::UNDEFINED if nil_type? + + self + end + # @return [String] def determine_non_literal_name # https://github.com/ruby/rbs/blob/master/docs/syntax.md From 8e0bf1a5597af30c507e4badead1c0a9e8b7bc90 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 14:09:36 -0400 Subject: [PATCH 301/524] Add spec for is_a? in a "break unless" statement --- spec/parser/flow_sensitive_typing_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 50b356f40..450eaa37c 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -189,6 +189,22 @@ class Repro < ReproBase; end 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] @@ -201,6 +217,7 @@ class Repro < ReproBase; end 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') @@ -225,6 +242,7 @@ def verify_repro(repr, throw_the_dice) end end ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') @@ -247,6 +265,7 @@ class Repro < ReproBase; end 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('ReproBase') @@ -263,6 +282,7 @@ class Repro < ReproBase; end 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('ReproBase') @@ -279,6 +299,7 @@ def baz; end 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') From 5cdedb8d70231e089e4cb1072fba2f9a4ec6b496 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 14:14:29 -0400 Subject: [PATCH 302/524] Fix merge issue --- spec/parser/flow_sensitive_typing_spec.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 450eaa37c..6953656b8 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -189,22 +189,6 @@ class Repro < ReproBase; end 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] From 97ffc4bec848e502cf69fb3ca86a1109efd06de1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 14:24:22 -0400 Subject: [PATCH 303/524] Add missing loose_unions location --- 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 d867dcd29..deeb7f031 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -241,7 +241,7 @@ def self.load_with_cache directory, out = nil, loose_unions: true end api_map.cache_all!(out) - load(directory) + load(directory, loose_unions: loose_unions) end # @return [Array] From 60fb1cb433e80b8c26d350e1bbb71b1127a72065 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 14:24:44 -0400 Subject: [PATCH 304/524] Move to enforcement at alpha level for now --- lib/solargraph/type_checker/rules.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 7abc7c3f2..4984b3efe 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -51,7 +51,7 @@ def must_tag_or_infer? end def loose_unions? - rank < LEVELS[:strong] + rank < LEVELS[:alpha] end def validate_tags? From f51954aa00689264f007619d5014acc0bbe9a4c8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 16:10:59 -0400 Subject: [PATCH 305/524] Allow filename to be nil for solargraph-rspec specs --- lib/solargraph/parser/parser_gem/class_methods.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 1cc40e33b..42868010e 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -12,17 +12,17 @@ module ClassMethods # can find relevant local variables later even if this is just # a subset of the file in question # @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})] - def parse_with_comments code, filename, starting_line = 0 + def parse_with_comments code, filename = nil, starting_line = 0 node = parse(code, filename, starting_line) comments = CommentRipper.new(code, filename, 0).parse [node, comments] end # @param code [String] - # @param filename [String] + # @param filename [String, nil] # @param starting_line [Integer] # @return [Parser::AST::Node] - def parse code, filename, starting_line = 0 + def parse code, filename = nil, starting_line = 0 buffer = ::Parser::Source::Buffer.new(filename, starting_line) buffer.source = code parser.parse(buffer) From d3c0185512a13d3d1f164522825ba2864e0dcdbd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 16:39:23 -0400 Subject: [PATCH 306/524] Use appraisal gem while running solargraph-rspec specs This change matches the change in https://github.com/lekemula/solargraph-rspec/pull/27 that now requires use of the appraisal gem to run soalrgraph-rspec's specs --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 1c633fda0..cece27ae9 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -129,6 +129,7 @@ jobs: echo "gem 'solargraph', path: '../solargraph'" >> Gemfile bundle config path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 + bundle exec appraisal install - name: Configure .solargraph.yml run: | cd ../solargraph-rspec @@ -137,11 +138,11 @@ jobs: run: | cd ../solargraph-rspec rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) - bundle exec solargraph gems $rspec_gems + bundle exec appraisal solargraph gems $rspec_gems - name: Run specs run: | cd ../solargraph-rspec - bundle exec rspec --format progress + bundle exec appraisal rspec --format progress run_solargraph_rails_specs: # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem From e376dc543cb793aa1f166ba1cb9850a1e984b174 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 16:39:23 -0400 Subject: [PATCH 307/524] Use appraisal gem while running solargraph-rspec specs This change matches the change in https://github.com/lekemula/solargraph-rspec/pull/27 that now requires use of the appraisal gem to run soalrgraph-rspec's specs --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 1c633fda0..cece27ae9 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -129,6 +129,7 @@ jobs: echo "gem 'solargraph', path: '../solargraph'" >> Gemfile bundle config path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 + bundle exec appraisal install - name: Configure .solargraph.yml run: | cd ../solargraph-rspec @@ -137,11 +138,11 @@ jobs: run: | cd ../solargraph-rspec rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) - bundle exec solargraph gems $rspec_gems + bundle exec appraisal solargraph gems $rspec_gems - name: Run specs run: | cd ../solargraph-rspec - bundle exec rspec --format progress + bundle exec appraisal rspec --format progress run_solargraph_rails_specs: # check out solargraph-rails as well as this project, and point the former to use the latter as a local gem From da508cb3fc1a916ef694b35d610cfd4a1bd78479 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 20:27:55 -0400 Subject: [PATCH 308/524] Annotation fixes --- lib/solargraph/api_map.rb | 3 +++ lib/solargraph/pin_cache.rb | 2 ++ lib/solargraph/source/chain/call.rb | 1 + lib/solargraph/source/change.rb | 1 + 4 files changed, 7 insertions(+) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index eaa64b756..9b826fc97 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -658,8 +658,11 @@ def super_and_sub?(sup, sub) # @todo If two literals are different values of the same type, it would # make more sense for super_and_sub? to return true, but there are a # few callers that currently expect this to be false. + # @sg-ignore We should understand reassignment of variable to new type return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s + # @sg-ignore We should understand reassignment of variable to new type sup = sup.simplify_literals.to_s + # @sg-ignore We should understand reassignment of variable to new type sub = sub.simplify_literals.to_s return true if sup == sub sc_fqns = sub diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index b3c162a15..2fa48d0fa 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -219,6 +219,7 @@ def save file, pins end # @param path_segments [Array] + # @param out [IO, nil] # @return [void] def uncache *path_segments, out: nil path = File.join(*path_segments) @@ -229,6 +230,7 @@ def uncache *path_segments, out: nil end # @return [void] + # @param out [IO, nil] # @param path_segments [Array] def uncache_by_prefix *path_segments, out: nil path = File.join(*path_segments) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index bd8382851..0688643b4 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -242,6 +242,7 @@ def extra_return_type docstring, context def find_method_pin(name_pin) method_pin = name_pin until method_pin.is_a?(Pin::Method) + # @sg-ignore Reassignment as a function of itself issue method_pin = method_pin.closure return if method_pin.nil? end diff --git a/lib/solargraph/source/change.rb b/lib/solargraph/source/change.rb index 65c47c7e0..2f6d6ea17 100644 --- a/lib/solargraph/source/change.rb +++ b/lib/solargraph/source/change.rb @@ -61,6 +61,7 @@ def repair text off = Position.to_offset(text, range.start) match = result[0, off].match(/[.:]+\z/) if match + # @sg-ignore Reassignment as a function of itself issue result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..-1] end result From 08816d8ab6e37fec50b4b0702e98642a2d3b9b24 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 26 Oct 2025 10:27:39 -0400 Subject: [PATCH 309/524] Drop unused file --- lib/solargraph/pin/if.rb | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 lib/solargraph/pin/if.rb diff --git a/lib/solargraph/pin/if.rb b/lib/solargraph/pin/if.rb deleted file mode 100644 index 35b8a9bfc..000000000 --- a/lib/solargraph/pin/if.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - module Pin - class If < Base - include CompoundStatementable - - # @param receiver [Parser::AST::Node, nil] - # @param node [Parser::AST::Node, nil] - # @param context [ComplexType, nil] - # @param args [::Array] - def initialize node: nil, **splat - super(**splat) - @node = node - end - end - end -end From 27cd225ed18073c8fe1e50e22cdd4d03339803aa Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 26 Oct 2025 14:18:15 -0400 Subject: [PATCH 310/524] Ensure nullable? is on both UniqueType and ComplexType --- lib/solargraph/complex_type/unique_type.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 6b941d76d..68dafcb5f 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -248,6 +248,10 @@ def can_assign?(api_map, atype) out end + def nullable? + nil_type? + end + # @return [UniqueType] def downcast_to_literal_if_possible SINGLE_SUBTYPE.fetch(rooted_tag, self) From 89cd8b98ae0b256c9e2908dddf1d42c569a86945 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:29:07 -0400 Subject: [PATCH 311/524] Refactor --- lib/solargraph/parser/flow_sensitive_typing.rb | 4 ++-- lib/solargraph/parser/node_processor/base.rb | 4 ++-- .../parser/parser_gem/node_processors/begin_node.rb | 9 +++++++++ lib/solargraph/pin.rb | 4 ++-- lib/solargraph/pin/breakable.rb | 2 -- lib/solargraph/pin/closure.rb | 2 +- ...pound_statementable.rb => compound_statement.rb} | 13 +++++++++---- lib/solargraph/pin/method.rb | 1 - lib/solargraph/pin/until.rb | 2 +- lib/solargraph/pin/while.rb | 2 +- lib/solargraph/type_checker.rb | 2 +- 11 files changed, 28 insertions(+), 17 deletions(-) rename lib/solargraph/pin/{compound_statementable.rb => compound_statement.rb} (84%) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 42143d6a4..198f2bb15 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -5,7 +5,7 @@ class FlowSensitiveTyping # @param locals [Array] # @param enclosing_breakable_pin [Solargraph::Pin::Breakable, nil] - # @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatementable, nil] + # @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatement, nil] def initialize(locals, enclosing_breakable_pin, enclosing_compound_statement_pin) @locals = locals @enclosing_breakable_pin = enclosing_breakable_pin @@ -118,7 +118,7 @@ def process_if(if_node, true_ranges = [], false_ranges = []) end end - unless enclosing_compound_statement_pin.nil? + unless enclosing_compound_statement_pin.node.nil? rest_of_returnable_body = Range.new(get_node_end_position(if_node), get_node_end_position(enclosing_compound_statement_pin.node)) diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index 1a79bea65..594fbc1da 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -57,9 +57,9 @@ def enclosing_breakable_pin end # @sg-ignore downcast output of Enumerable#select - # @return [Solargraph::Pin::CompoundStatementable, nil] + # @return [Solargraph::Pin::CompoundStatement, nil] def enclosing_compound_statement_pin - pins.select{|pin| pin.is_a?(Pin::CompoundStatementable) && pin.location&.range&.contain?(position)}.last + pins.select{|pin| pin.is_a?(Pin::CompoundStatement) && pin.location&.range&.contain?(position)}.last end # @param subregion [Region] diff --git a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb index b52b9d3c6..426c751f6 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb @@ -6,6 +6,15 @@ module ParserGem module NodeProcessors class BeginNode < Parser::NodeProcessor::Base def process + # Note: we intentionally don't create a CompoundStatement + # pin here, as this is not necessarily a control flow + # block - e.g., a begin...end without rescue or ensure + # should be treated by flow-sensitive typing as if the + # begin and end didn't exist at all. As such, we create + # the CompoundStatement pins around the things which + # actually result in control flow changes - like + # if/while/rescue/etc + process_children end end diff --git a/lib/solargraph/pin.rb b/lib/solargraph/pin.rb index 016550966..6cd6fcaf9 100644 --- a/lib/solargraph/pin.rb +++ b/lib/solargraph/pin.rb @@ -38,8 +38,8 @@ module Pin autoload :Until, 'solargraph/pin/until' autoload :While, 'solargraph/pin/while' autoload :Callable, 'solargraph/pin/callable' - autoload :CompoundStatementable, - 'solargraph/pin/compound_statementable' + autoload :CompoundStatement, + 'solargraph/pin/compound_statement' ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil, source: :pin_rb) end diff --git a/lib/solargraph/pin/breakable.rb b/lib/solargraph/pin/breakable.rb index 5de67cf4a..7cf6df9ab 100644 --- a/lib/solargraph/pin/breakable.rb +++ b/lib/solargraph/pin/breakable.rb @@ -3,8 +3,6 @@ module Pin # Mix-in for pins which enclose code which the 'break' statement # works with-in - e.g., blocks, when, until, ... module Breakable - include CompoundStatementable - # @return [Parser::AST::Node] attr_reader :node diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index a7b37e01b..347e0229e 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -2,7 +2,7 @@ module Solargraph module Pin - class Closure < Base + class Closure < CompoundStatement # @return [::Symbol] :class or :instance attr_reader :scope diff --git a/lib/solargraph/pin/compound_statementable.rb b/lib/solargraph/pin/compound_statement.rb similarity index 84% rename from lib/solargraph/pin/compound_statementable.rb rename to lib/solargraph/pin/compound_statement.rb index debad6615..4598d677a 100644 --- a/lib/solargraph/pin/compound_statementable.rb +++ b/lib/solargraph/pin/compound_statement.rb @@ -39,12 +39,17 @@ module Pin # Just because statement #1 in a sequence is executed, it doesn't # mean that future ones will. Consider the effect of # break/next/return/raise/etc. on control flow. - module CompoundStatementable - # @return [Parser::AST::Node] + class CompoundStatement < Pin::Base attr_reader :node - # @return [Location, nil] - attr_reader :location + # @param receiver [Parser::AST::Node, nil] + # @param node [Parser::AST::Node, nil] + # @param context [ComplexType, nil] + # @param args [::Array] + def initialize node: nil, **splat + super(**splat) + @node = node + end end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 871708253..011f096f6 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -6,7 +6,6 @@ module Pin # class Method < Callable include Solargraph::Parser::NodeMethods - include CompoundStatementable # @return [::Symbol] :public, :private, or :protected attr_reader :visibility diff --git a/lib/solargraph/pin/until.rb b/lib/solargraph/pin/until.rb index 67823532b..7e050fea6 100644 --- a/lib/solargraph/pin/until.rb +++ b/lib/solargraph/pin/until.rb @@ -2,7 +2,7 @@ module Solargraph module Pin - class Until < Base + class Until < CompoundStatement include Breakable # @param receiver [Parser::AST::Node, nil] diff --git a/lib/solargraph/pin/while.rb b/lib/solargraph/pin/while.rb index e380aadd9..ac8c31c97 100644 --- a/lib/solargraph/pin/while.rb +++ b/lib/solargraph/pin/while.rb @@ -2,7 +2,7 @@ module Solargraph module Pin - class While < Base + class While < CompoundStatement include Breakable # @param receiver [Parser::AST::Node, nil] diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 4600767b5..1ca0bc602 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -192,6 +192,7 @@ def ignored_pins def variable_type_tag_problems result = [] all_variables.each do |pin| + # @sg-ignore Need to add nil check here if pin.return_type.defined? declared = pin.typify(api_map) next if declared.duck_type? @@ -308,7 +309,6 @@ def call_problems def argument_problems_for chain, api_map, closure_pin, locals, location result = [] base = chain - # @type last_base_link [Solargraph::Source::Chain::Call] last_base_link = base.links.last return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call) From 6b2afc996e47d303116e6542fdecbc55b7341838 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:31:17 -0400 Subject: [PATCH 312/524] Fix comment style --- .../parser_gem/node_processors/begin_node.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb index 426c751f6..19e53a681 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb @@ -6,13 +6,13 @@ module ParserGem module NodeProcessors class BeginNode < Parser::NodeProcessor::Base def process - # Note: we intentionally don't create a CompoundStatement - # pin here, as this is not necessarily a control flow - # block - e.g., a begin...end without rescue or ensure - # should be treated by flow-sensitive typing as if the - # begin and end didn't exist at all. As such, we create - # the CompoundStatement pins around the things which - # actually result in control flow changes - like + # We intentionally don't create a CompoundStatement pin + # here, as this is not necessarily a control flow block - + # e.g., a begin...end without rescue or ensure should be + # treated by flow-sensitive typing as if the begin and end + # didn't exist at all. As such, we create the + # CompoundStatement pins around the things which actually + # result in control flow changes - like # if/while/rescue/etc process_children From 3d390267e69c4cf484ddfb37d2595180514f5b7e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:41:11 -0400 Subject: [PATCH 313/524] Update Gemfile.lock in solargraph-rspec --- .github/workflows/plugins.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index cece27ae9..03617884a 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -94,6 +94,12 @@ jobs: echo 'gem "solargraph-rspec"' >> .Gemfile bundle install bundle update rbs + # /opt/hostedtoolcache/Ruby/3.4.7/x64/lib/ruby/3.4.0/bundler/runtime.rb:317:in + # 'Bundler::Runtime#check_for_activated_spec!': You have + # already activated date 3.5.0, but your Gemfile requires date + # 3.4.1. Prepending `bundle exec` to your command may solve + # this. (Gem::LoadError) + bundle update date - name: Configure to use plugins run: | bundle exec solargraph config From bd075c52624daf9de433746857a1850eac888b77 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:43:52 -0400 Subject: [PATCH 314/524] Drop @sg-ignore --- lib/solargraph/parser/node_processor/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/parser/node_processor/base.rb b/lib/solargraph/parser/node_processor/base.rb index 594fbc1da..76f61c5e4 100644 --- a/lib/solargraph/parser/node_processor/base.rb +++ b/lib/solargraph/parser/node_processor/base.rb @@ -56,7 +56,7 @@ def enclosing_breakable_pin pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position)}.last end - # @sg-ignore downcast output of Enumerable#select + # @todo downcast output of Enumerable#select # @return [Solargraph::Pin::CompoundStatement, nil] def enclosing_compound_statement_pin pins.select{|pin| pin.is_a?(Pin::CompoundStatement) && pin.location&.range&.contain?(position)}.last From 63196306991ff25d1baebf5eed50373713693053 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:47:01 -0400 Subject: [PATCH 315/524] Update Gemfile.lock in solargraph-rspec --- .github/workflows/plugins.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 03617884a..ae8355bf7 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -94,12 +94,6 @@ jobs: echo 'gem "solargraph-rspec"' >> .Gemfile bundle install bundle update rbs - # /opt/hostedtoolcache/Ruby/3.4.7/x64/lib/ruby/3.4.0/bundler/runtime.rb:317:in - # 'Bundler::Runtime#check_for_activated_spec!': You have - # already activated date 3.5.0, but your Gemfile requires date - # 3.4.1. Prepending `bundle exec` to your command may solve - # this. (Gem::LoadError) - bundle update date - name: Configure to use plugins run: | bundle exec solargraph config @@ -135,7 +129,13 @@ jobs: echo "gem 'solargraph', path: '../solargraph'" >> Gemfile bundle config path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 - bundle exec appraisal install + # /opt/hostedtoolcache/Ruby/3.4.7/x64/lib/ruby/3.4.0/bundler/runtime.rb:317:in + # 'Bundler::Runtime#check_for_activated_spec!': You have + # already activated date 3.5.0, but your Gemfile requires date + # 3.4.1. Prepending `bundle exec` to your command may solve + # this. (Gem::LoadError) + bundle update date + bundle exec appraisal install - name: Configure .solargraph.yml run: | cd ../solargraph-rspec @@ -143,6 +143,7 @@ jobs: - name: Solargraph generate RSpec gems YARD and RBS pins run: | cd ../solargraph-rspec + rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) bundle exec appraisal solargraph gems $rspec_gems - name: Run specs From 1b12f8f742171ac2872f6ec97e2c21eb21b6f8aa Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 07:57:09 -0400 Subject: [PATCH 316/524] Update Gemfile.lock in solargraph-rspec --- .github/workflows/plugins.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index ae8355bf7..dffa58628 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -125,17 +125,18 @@ jobs: bundler-cache: false - name: Install gems run: | - cd ../solargraph-rspec - echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - bundle config path ${{ env.BUNDLE_PATH }} - bundle install --jobs 4 --retry 3 + cd ../solargraph-rspec + echo "gem 'solargraph', path: '../solargraph'" >> Gemfile + bundle config path ${{ env.BUNDLE_PATH }} + bundle install --jobs 4 --retry 3 + bundle exec appraisal install # /opt/hostedtoolcache/Ruby/3.4.7/x64/lib/ruby/3.4.0/bundler/runtime.rb:317:in # 'Bundler::Runtime#check_for_activated_spec!': You have # already activated date 3.5.0, but your Gemfile requires date # 3.4.1. Prepending `bundle exec` to your command may solve # this. (Gem::LoadError) - bundle update date bundle exec appraisal install + bundle exec appraisal update date - name: Configure .solargraph.yml run: | cd ../solargraph-rspec From 8c5c5fb623dcd70380814aa57176b8e92d44b6d8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 08:01:08 -0400 Subject: [PATCH 317/524] Better handling of 'return if' --- .../parser_gem/node_processors/if_node.rb | 35 +++++++++++++++++-- spec/parser/flow_sensitive_typing_spec.rb | 4 --- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb index e59fa4baa..9b0bbd978 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/if_node.rb @@ -8,11 +8,42 @@ class IfNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - process_children - FlowSensitiveTyping.new(locals, enclosing_breakable_pin, enclosing_compound_statement_pin).process_if(node) + condition_node = node.children[0] + if condition_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(condition_node), + closure: region.closure, + node: condition_node, + source: :parser, + ) + NodeProcessor.process(condition_node, region, pins, locals) + end + then_node = node.children[1] + if then_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(then_node), + closure: region.closure, + node: then_node, + source: :parser, + ) + NodeProcessor.process(then_node, region, pins, locals) + end + + else_node = node.children[2] + if else_node + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(else_node), + closure: region.closure, + node: else_node, + source: :parser, + ) + NodeProcessor.process(else_node, region, pins, locals) + end + + true end end end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 6953656b8..33e29f53b 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -695,8 +695,6 @@ def bar(baz: nil) clip = api_map.clip_at('test.rb', [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - pending('better scoping of return if in unless') - clip = api_map.clip_at('test.rb', [10, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -826,8 +824,6 @@ def bar(baz: nil) clip = api_map.clip_at('test.rb', [6, 44]) expect(clip.infer.rooted_tags).to eq('::Boolean') - pending('better scoping of return if in ternary operator') - clip = api_map.clip_at('test.rb', [6, 51]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') From aff5fbf55bb14f020ffaca27ef9d099dd802c578 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 31 Oct 2025 08:16:34 -0400 Subject: [PATCH 318/524] Better handling of 'case when' --- .../parser/parser_gem/node_processors.rb | 2 ++ .../parser_gem/node_processors/when_node.rb | 23 +++++++++++++++++++ spec/parser/flow_sensitive_typing_spec.rb | 2 -- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 lib/solargraph/parser/parser_gem/node_processors/when_node.rb diff --git a/lib/solargraph/parser/parser_gem/node_processors.rb b/lib/solargraph/parser/parser_gem/node_processors.rb index e2cb828da..e80970344 100644 --- a/lib/solargraph/parser/parser_gem/node_processors.rb +++ b/lib/solargraph/parser/parser_gem/node_processors.rb @@ -27,6 +27,7 @@ module NodeProcessors autoload :SymNode, 'solargraph/parser/parser_gem/node_processors/sym_node' autoload :ResbodyNode, 'solargraph/parser/parser_gem/node_processors/resbody_node' autoload :UntilNode, 'solargraph/parser/parser_gem/node_processors/until_node' + autoload :WhenNode, 'solargraph/parser/parser_gem/node_processors/when_node' autoload :WhileNode, 'solargraph/parser/parser_gem/node_processors/while_node' autoload :AndNode, 'solargraph/parser/parser_gem/node_processors/and_node' end @@ -65,6 +66,7 @@ module NodeProcessor register :until, ParserGem::NodeProcessors::UntilNode register :while, ParserGem::NodeProcessors::WhileNode register :and, ParserGem::NodeProcessors::AndNode + register :when, ParserGem::NodeProcessors::WhenNode end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/when_node.rb b/lib/solargraph/parser/parser_gem/node_processors/when_node.rb new file mode 100644 index 000000000..b2b11dec1 --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/when_node.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class WhenNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + pins.push Solargraph::Pin::CompoundStatement.new( + location: get_node_location(node), + closure: region.closure, + node: node, + source: :parser, + ) + process_children + end + end + end + end + end +end diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 33e29f53b..01d263cb3 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -795,8 +795,6 @@ def bar(baz: nil) clip = api_map.clip_at('test.rb', [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - pending('better scoping of return if in case/when') - clip = api_map.clip_at('test.rb', [10, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') From ba66987a91ab7ac77c973c5a3dd424fcc8020e45 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 12:37:48 -0400 Subject: [PATCH 319/524] Support intersection types See https://en.wikipedia.org/wiki/Intersection_type --- .rubocop_todo.yml | 15 +- lib/solargraph/complex_type.rb | 34 ++++ lib/solargraph/complex_type/unique_type.rb | 41 +++- .../parser/flow_sensitive_typing.rb | 23 ++- lib/solargraph/pin/base_variable.rb | 188 +++++++++++++++++- lib/solargraph/pin/local_variable.rb | 44 ++-- lib/solargraph/pin/parameter.rb | 13 +- spec/pin/base_variable_spec.rb | 15 ++ 8 files changed, 330 insertions(+), 43 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83339e756..bb1d0cf9f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -34,7 +34,6 @@ Gemspec/OrderedDependencies: # Configuration parameters: Severity. Gemspec/RequireMFA: Exclude: - - 'solargraph.gemspec' - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - 'spec/fixtures/rubocop-custom-version/specifications/rubocop-0.0.0.gemspec' @@ -1230,7 +1229,12 @@ Style/TrailingCommaInArrayLiteral: # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInHashLiteral: - Enabled: false + Exclude: + - 'lib/solargraph/pin/base_variable.rb' + - 'lib/solargraph/pin/callable.rb' + - 'lib/solargraph/pin/closure.rb' + - 'lib/solargraph/pin/parameter.rb' + - 'lib/solargraph/rbs_map/conversions.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. @@ -1278,12 +1282,7 @@ YARD/MismatchName: Enabled: false YARD/TagTypeSyntax: - Exclude: - - 'lib/solargraph/api_map/constants.rb' - - 'lib/solargraph/language_server/host.rb' - - 'lib/solargraph/parser/comment_ripper.rb' - - 'lib/solargraph/pin/method.rb' - - 'lib/solargraph/type_checker.rb' + Enabled: false # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 669a66900..6539ae59d 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -271,6 +271,40 @@ def rooted? @items.all?(&:rooted?) end + # @param exclude_types [ComplexType, nil] + # @param api_map [ApiMap] + # @return [ComplexType, self] + def exclude exclude_types, api_map + return self if exclude_types.nil? + + types = items - exclude_types.items + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + + # @see https://en.wikipedia.org/wiki/Intersection_type + # + # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param api_map [ApiMap] + # @return [self, ComplexType::UniqueType] + def intersect_with intersection_type, api_map + return self if intersection_type.nil? + return intersection_type if undefined? + types = [] + # try to find common types via conformance + items.each do |ut| + intersection_type.each do |int_type| + if ut.can_assign?(api_map, int_type) + types << int_type + elsif int_type.can_assign?(api_map, ut) + types << ut + end + end + end + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + protected # @return [ComplexType] diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 05a585dcf..dc386cd24 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -109,6 +109,40 @@ def simplify_literals end end + # @param exclude_types [ComplexType, nil] + # @param api_map [ApiMap] + # @return [ComplexType, self] + def exclude exclude_types, api_map + return self if exclude_types.nil? + + types = items - exclude_types.items + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + + # @see https://en.wikipedia.org/wiki/Intersection_type + # + # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param api_map [ApiMap] + # @return [self, ComplexType] + def intersect_with intersection_type, api_map + return self if intersection_type.nil? + return intersection_type if undefined? + types = [] + # try to find common types via conformance + items.each do |ut| + intersection_type.each do |int_type| + if ut.can_assign?(api_map, int_type) + types << int_type + elsif int_type.can_assign?(api_map, ut) + types << ut + end + end + end + types = [ComplexType::UniqueType::UNDEFINED] if types.empty? + ComplexType.new(types) + end + def literal? non_literal_name != name end @@ -237,7 +271,7 @@ def generic? end # @param api_map [ApiMap] The ApiMap that performs qualification - # @param atype [ComplexType] type which may be assigned to this type + # @param atype [ComplexType, self] 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 @@ -248,6 +282,11 @@ def can_assign?(api_map, atype) out end + # @yieldreturn [Boolean] + def all? &block + block.yield self + end + # @return [UniqueType] def downcast_to_literal_if_possible SINGLE_SUBTYPE.fetch(rooted_tag, self) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 41ce6eeaf..213f8a779 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -120,11 +120,12 @@ def self.visible_pins(pins, name, closure, location) private # @param pin [Pin::LocalVariable] - # @param downcast_type_name [String] # @param presence [Range] + # @param downcast_type [ComplexType, nil] + # @param downcast_not_type [ComplexType, nil] # # @return [void] - def add_downcast_local(pin, downcast_type_name, presence) + def add_downcast_local(pin, presence:, downcast_type:, downcast_not_type:) # @todo Create pin#update method new_pin = Solargraph::Pin::LocalVariable.new( location: pin.location, @@ -133,14 +134,15 @@ def add_downcast_local(pin, downcast_type_name, presence) assignment: pin.assignment, comments: pin.comments, presence: presence, - return_type: ComplexType.try_parse(downcast_type_name), - presence_certain: true, + intersection_return_type: downcast_type, + exclude_return_type: downcast_not_type, + return_type: pin.return_type, source: :flow_sensitive_typing ) locals.push(new_pin) end - # @param facts_by_pin [Hash{Pin::LocalVariable => Array String}>}] + # @param facts_by_pin [Hash{Pin::LocalVariable => Array ComplexType}>}] # @param presences [Array] # # @return [void] @@ -150,9 +152,13 @@ def process_facts(facts_by_pin, presences) # facts_by_pin.each_pair do |pin, facts| facts.each do |fact| - downcast_type_name = fact.fetch(:type) + downcast_type = fact.fetch(:type, nil) + downcast_not_type = fact.fetch(:not_type, nil) presences.each do |presence| - add_downcast_local(pin, downcast_type_name, presence) + add_downcast_local(pin, + presence: presence, + downcast_type: downcast_type, + downcast_not_type: downcast_not_type) end end end @@ -218,9 +224,10 @@ def process_isa(isa_node, true_presences) pin = find_local(variable_name, isa_position) return unless pin + # @type Hash{Pin::LocalVariable => Array ComplexType}>} if_true = {} if_true[pin] ||= [] - if_true[pin] << { type: isa_type_name } + if_true[pin] << { type: ComplexType.parse(isa_type_name) } process_facts(if_true, true_presences) end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 764c1fb39..cea325fcb 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -13,12 +13,37 @@ class BaseVariable < Base # @param return_type [ComplexType, nil] # @param assignment [Parser::AST::Node, nil] - def initialize assignment: nil, return_type: nil, **splat + # @param exclude_return_type [ComplexType, nil] Ensure any + # return type returned will never include any of these unique + # types in the unique types of its complex type. + # + # Example: If a return type is 'Float | Integer | nil' and the + # exclude_return_type is 'Integer', the resulting return + # type will be 'Float | nil' because Integer is excluded. + # @param intersection_return_type [ComplexType, nil] Ensure each unique + # return type is compatible with at least one element of this + # complex type. If a ComplexType used as a return type is an + # union type - we can return any of these - these are + # intersection types - everything we return needs to meet at least + # one of these unique types. + # + # Example: If a return type is 'Numeric | nil' and the + # intersection_return_type is 'Float | nil', the resulting return + # type will be 'Float | nil' because Float is compatible + # with Numeric and nil is compatible with nil. + # @see https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types + # @see https://en.wikipedia.org/wiki/Intersection_type#TypeScript_example + # @param mass_assignment [Array(Parser::AST::Node, Integer), nil] + def initialize assignment: nil, mass_assignment: nil, return_type: nil, + intersection_return_type: nil, exclude_return_type: nil, + **splat super(**splat) @assignment = assignment # @type [nil, ::Array(Parser::AST::Node, Integer)] @mass_assignment = nil @return_type = return_type + @intersection_return_type = intersection_return_type + @exclude_return_type = exclude_return_type end def combine_with(other, attrs={}) @@ -26,10 +51,32 @@ def combine_with(other, attrs={}) assignment: assert_same(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + intersection_return_type: combine_types(other, :intersection_return_type), + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + exclude_return_type: combine_types(other, :exclude_return_type), }) super(other, attrs) end + def reset_generated! + @return_type_minus_exclusions = nil + super + end + + def inner_desc + super + ", intersection_return_type=#{intersection_return_type&.rooted_tags.inspect}, exclude_return_type=#{exclude_return_type&.rooted_tags.inspect}" + end + + # @param other [self] + # + # @return [Array(AST::Node, Integer), nil] + def combine_mass_assignment(other) + # @todo pick first non-nil arbitrarily - we don't yet support + # mass assignment merging + mass_assignment || other.mass_assignment + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::VARIABLE end @@ -39,10 +86,6 @@ def symbol_kind Solargraph::LanguageServer::SymbolKinds::VARIABLE end - def return_type - @return_type ||= generate_complex_type - end - def nil_assignment? # this will always be false - should it be return_type == # ComplexType::NIL or somesuch? @@ -78,13 +121,16 @@ def return_types_from_node(parent_node, api_map) end # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def probe api_map unless @assignment.nil? types = return_types_from_node(@assignment, api_map) - return ComplexType.new(types.uniq) unless types.empty? + return adjust_type api_map, ComplexType.new(types.uniq) unless types.empty? end + # @todo should handle merging types from mass assignments as + # well so that we can do better flow sensitive typing with + # multiple assignments unless @mass_assignment.nil? mass_node, index = @mass_assignment types = return_types_from_node(mass_node, api_map) @@ -95,7 +141,10 @@ def probe api_map type.all_params.first end end.compact! - return ComplexType.new(types.uniq) unless types.empty? + + return ComplexType::UNDEFINED if types.empty? + + return adjust_type api_map, ComplexType.new(types.uniq).qualify(api_map, *gates) end ComplexType::UNDEFINED @@ -111,13 +160,132 @@ def type_desc "#{super} = #{assignment&.type.inspect}" end + # @return [ComplexType, nil] + def return_type + generate_complex_type || @return_type || intersection_return_type || ComplexType::UNDEFINED + end + + def typify api_map + raw_return_type = super + + adjust_type(api_map, raw_return_type) + end + + # @sg-ignore need boolish support for ? methods + def presence_certain? + exclude_return_type || intersection_return_type + end + private - # @return [ComplexType] + attr_reader :exclude_return_type, :intersection_return_type + + # @param api_map [ApiMap] + # @param raw_return_type [ComplexType, ComplexType::UniqueType] + # + # @return [ComplexType, ComplexType::UniqueType] + def adjust_type(api_map, raw_return_type) + qualified_exclude = exclude_return_type&.qualify(api_map, *(closure&.gates || [''])) + minus_exclusions = raw_return_type.exclude qualified_exclude, api_map + qualified_intersection = intersection_return_type&.qualify(api_map, *(closure&.gates || [''])) + minus_exclusions.intersect_with qualified_intersection, api_map + end + + # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure(other) + return closure if self.closure == other.closure + + # choose first defined, as that establishes the scope of the variable + if closure.nil? || other.closure.nil? + Solargraph.assert_or_log(:varible_closure_missing) do + "One of the local variables being combined is missing a closure: " \ + "#{self.inspect} vs #{other.inspect}" + end + return closure || other.closure + end + + if closure.location.nil? || other.closure.location.nil? + return closure.location.nil? ? other.closure : closure + end + + # if filenames are different, this will just pick one + # @sg-ignore flow sensitive typing needs to handle ivars + return closure if closure.location <= other.closure.location + + other.closure + end + + # See if this variable is visible within 'other_closure' + # + # @param other_closure [Pin::Closure] + # @return [Boolean] + def visible_in_closure? other_closure + needle = closure + return false if closure.nil? + haystack = other_closure + + cursor = haystack + + until cursor.nil? + if cursor.is_a?(Pin::Method) && closure.context.tags == 'Class<>' + # methods can't see local variables declared in their + # parent closure + return false + end + + if cursor.binder.namespace == needle.binder.namespace + return true + end + + if cursor.return_type == needle.context + return true + end + + if scope == :instance && cursor.is_a?(Pin::Namespace) + # classes and modules can't see local variables declared + # in their parent closure, so stop here + return false + end + + cursor = cursor.closure + end + false + end + + # @param other [self] + # @return [ComplexType, nil] + def combine_return_type(other) + combine_types(other, :return_type) + end + + # @param other [self] + # @param attr [::Symbol] + # + # @return [ComplexType, nil] + def combine_types(other, attr) + # @type [ComplexType, nil] + type1 = send(attr) + # @type [ComplexType, nil] + type2 = other.send(attr) + if type1 && type2 + types = (type1.items + type2.items).uniq + ComplexType.new(types) + else + type1 || type2 + end + end + + # @return [::Symbol] + def scope + :instance + end + + # @return [ComplexType, nil] def generate_complex_type tag = docstring.tag(:type) return ComplexType.try_parse(*tag.types) unless tag.nil? || tag.types.nil? || tag.types.empty? - ComplexType.new + nil end end end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 9eae6cc6f..81baca396 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -6,26 +6,32 @@ class LocalVariable < BaseVariable # @return [Range] attr_reader :presence - def presence_certain? - @presence_certain - end - - # @param assignment [AST::Node, nil] # @param presence [Range, nil] - # @param presence_certain [Boolean] # @param splat [Hash] - def initialize assignment: nil, presence: nil, presence_certain: false, **splat + def initialize presence: nil, + **splat super(**splat) - @assignment = assignment @presence = presence - @presence_certain = presence_certain + end + + # @param api_map [ApiMap] + # @return [ComplexType] + def probe api_map + if presence_certain? && return_type&.defined? + # flow sensitive typing has already figured out this type + # has been downcast - use the type it figured out + return adjust_type api_map, return_type.qualify(api_map, *gates) + end + + super + end + + def inner_desc + super + ", presence=#{presence.inspect}" end def combine_with(other, attrs={}) - new_attrs = { - assignment: assert_same(other, :assignment), - presence_certain: assert_same(other, :presence_certain?), - }.merge(attrs) + new_attrs = {}.merge(attrs) # @sg-ignore Wrong argument type for # Solargraph::Pin::Base#assert_same: other expected # Solargraph::Pin::Base, received self @@ -36,12 +42,22 @@ def combine_with(other, attrs={}) # @param other_closure [Pin::Closure] # @param other_loc [Location] + # @sg-ignore Need to add nil check here def visible_at?(other_closure, other_loc) + # @sg-ignore Need to add nil check here location.filename == other_loc.filename && - presence.include?(other_loc.range.start) && + presence&.include?(other_loc.range.start) && + # @sg-ignore Need to add nil check here match_named_closure(other_closure, closure) end + # @param other_loc [Location] + def starts_at?(other_loc) + location&.filename == other_loc.filename && + presence && + presence.start == other_loc.range.start + end + def to_rbs (name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped') end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 947513689..797f7b5da 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -166,8 +166,13 @@ def index # @param api_map [ApiMap] def typify api_map - return return_type.qualify(api_map, *closure.gates) unless return_type.undefined? - closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) + new_type = super + return new_type if new_type.defined? + + # sniff based on param tags + new_type = closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) + + adjust_type api_map, new_type end # @param atype [ComplexType] @@ -187,6 +192,10 @@ def documentation private + def generate_complex_type + nil + end + # @return [YARD::Tags::Tag, nil] def param_tag params = closure.docstring.tags(:param) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 8c462bff3..7a72670e5 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -44,4 +44,19 @@ def bar expect(type.to_rbs).to eq('(1 | nil)') expect(type.simplify_literals.to_rbs).to eq('(::Integer | ::NilClass)') end + + it "understands parameters aren't affected by @type" do + code = %( + # @return [Proc] + def foo + # @type [Proc] + # @param layout [Boolean] + @render_method = proc { |layout = false| + 123 if layout + } + end + ) + checker = Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + expect(checker.problems.map(&:message)).to eq([]) + end end From e584d33b4a1a5aeffe6f9b01e582fb5c10802d8e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 13:22:14 -0400 Subject: [PATCH 320/524] Fix up param type --- lib/solargraph/rbs_map/conversions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 3e777f726..32800b254 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -726,7 +726,7 @@ def type_tag(type_name, type_args = []) build_type(type_name, type_args).tags end - # @param type [RBS::Types::Bases::Base] + # @param type [Object] # @return [String] def other_type_to_tag type if type.is_a?(RBS::Types::Optional) From bfe7f4e73f627433cbeb9851072490cdb046e314 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 16:54:49 -0400 Subject: [PATCH 321/524] Add downcast method, another falsey type --- .../parser/flow_sensitive_typing.rb | 20 +++++-------------- lib/solargraph/pin/base_variable.rb | 19 ++++++++++++++++-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 9a81f71c1..9f19b78f1 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -249,20 +249,10 @@ def self.visible_pins(pins, name, closure, location) # # @return [void] def add_downcast_local(pin, presence:, downcast_type:, downcast_not_type:) - # @todo Create pin#update method - new_pin = Solargraph::Pin::LocalVariable.new( - location: pin.location, - closure: pin.closure, - name: pin.name, - assignment: pin.assignment, - comments: pin.comments, - presence: presence, - intersection_return_type: downcast_type, - exclude_return_type: downcast_not_type, - return_type: pin.return_type, - source: :flow_sensitive_typing - ) - new_pin.reset_generated! + new_pin = pin.downcast(exclude_return_type: downcast_not_type, + intersection_return_type: downcast_type, + source: :flow_sensitive_typing, + presence: presence) locals.push(new_pin) end @@ -458,7 +448,7 @@ def process_variable(node, true_presences, false_presences) if_false = {} if_false[pin] ||= [] - if_false[pin] << { type: ComplexType::NIL } + if_false[pin] << { type: ComplexType.parse('nil, false') } process_facts(if_false, false_presences) end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index cea325fcb..60cb20bbf 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -46,6 +46,17 @@ def initialize assignment: nil, mass_assignment: nil, return_type: nil, @exclude_return_type = exclude_return_type end + def downcast exclude_return_type: nil, intersection_return_type: nil, + presence: presence, source: source + result = dup + result.exclude_return_type = exclude_return_type + result.intersection_return_type = intersection_return_type + result.source = source + result.presence = presence + result.reset_generated! + result + end + def combine_with(other, attrs={}) attrs.merge({ assignment: assert_same(other, :assignment), @@ -176,9 +187,13 @@ def presence_certain? exclude_return_type || intersection_return_type end - private + protected - attr_reader :exclude_return_type, :intersection_return_type + attr_accessor :exclude_return_type, :intersection_return_type + + attr_writer :presence + + private # @param api_map [ApiMap] # @param raw_return_type [ComplexType, ComplexType::UniqueType] From 19a81fe14010d50facc6169df7c9040825779712 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 16:57:21 -0400 Subject: [PATCH 322/524] Fix types --- lib/solargraph/pin/base_variable.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 60cb20bbf..8615c60ea 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -46,8 +46,12 @@ def initialize assignment: nil, mass_assignment: nil, return_type: nil, @exclude_return_type = exclude_return_type end + # @param exclude_return_type [ComplexType, nil] + # @param intersection_return_type [ComplexType, nil] + # @param presence [Range] + # @param source [Symbol] def downcast exclude_return_type: nil, intersection_return_type: nil, - presence: presence, source: source + presence:, source: self.source result = dup result.exclude_return_type = exclude_return_type result.intersection_return_type = intersection_return_type From 090099b51a1afaae804ad7338e89e0cecfe8daea Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 17:00:04 -0400 Subject: [PATCH 323/524] Fix types --- lib/solargraph/pin/base_variable.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 8615c60ea..5c4a646cd 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -46,12 +46,12 @@ def initialize assignment: nil, mass_assignment: nil, return_type: nil, @exclude_return_type = exclude_return_type end + # @param presence [Range] # @param exclude_return_type [ComplexType, nil] # @param intersection_return_type [ComplexType, nil] - # @param presence [Range] # @param source [Symbol] - def downcast exclude_return_type: nil, intersection_return_type: nil, - presence:, source: self.source + def downcast presence:, exclude_return_type: nil, intersection_return_type: nil, + source: self.source result = dup result.exclude_return_type = exclude_return_type result.intersection_return_type = intersection_return_type From a2cffcc400b24cb4e96ea278233409b88f597e5b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 2 Nov 2025 08:26:24 -0500 Subject: [PATCH 324/524] Fix tags --- lib/solargraph/pin/base_variable.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 5c4a646cd..518be8a83 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -49,7 +49,9 @@ def initialize assignment: nil, mass_assignment: nil, return_type: nil, # @param presence [Range] # @param exclude_return_type [ComplexType, nil] # @param intersection_return_type [ComplexType, nil] - # @param source [Symbol] + # @param source [::Symbol] + # + # @return [self] def downcast presence:, exclude_return_type: nil, intersection_return_type: nil, source: self.source result = dup @@ -195,6 +197,7 @@ def presence_certain? attr_accessor :exclude_return_type, :intersection_return_type + # @return [Range] attr_writer :presence private From b78965b99402183ab54de972a784440596d88e37 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 09:21:11 -0500 Subject: [PATCH 325/524] Don't set binder to avoid trigger solargraph-rspec bug --- .../parser/parser_gem/node_processors/def_node.rb | 11 ++++------- lib/solargraph/pin/base.rb | 4 ---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb index 309de01a1..c4e4f9a70 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -8,13 +8,12 @@ class DefNode < Parser::NodeProcessor::Base def process name = node.children[0].to_s scope = region.scope || (region.closure.is_a?(Pin::Singleton) ? :class : :instance) - method_binder = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder + method_context = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder methpin = Solargraph::Pin::Method.new( location: get_node_location(node), closure: region.closure, name: name, - context: method_binder, - binder: method_binder, + context: method_context, comments: comments_for(node), scope: scope, visibility: scope == :instance && name == 'initialize' ? :private : region.visibility, @@ -26,8 +25,7 @@ def process location: methpin.location, closure: methpin.closure, name: methpin.name, - context: region.closure.binder, - binder: region.closure.binder, + context: method_context, comments: methpin.comments, scope: :class, visibility: :public, @@ -39,8 +37,7 @@ def process location: methpin.location, closure: methpin.closure, name: methpin.name, - context: region.closure.binder, - binder: region.closure.binder, + context: method_context, comments: methpin.comments, scope: :instance, visibility: :private, diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 6ff60e2cd..7acf9d039 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -649,10 +649,6 @@ def all_location_text end end - # @return [void] - def reset_generated! - end - protected # @return [Boolean] From ed2381371b008165a1c2fdb992bcac7bda1d6a23 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 09:25:02 -0500 Subject: [PATCH 326/524] Reproduce solargraph-rspec rspec failure --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..2619b9ff8 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -143,6 +143,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: + # solargraph-rails supports Ruby 3.0+ ruby-version: '3.0' bundler-cache: false From 769041b0dac0ec5024b1582abb507a2a740b27cd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 09:36:46 -0500 Subject: [PATCH 327/524] Add set -x to debug --- .github/workflows/plugins.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index cece27ae9..1d698f43a 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -125,6 +125,8 @@ jobs: bundler-cache: false - name: Install gems run: | + set -x + cd ../solargraph-rspec echo "gem 'solargraph', path: '../solargraph'" >> Gemfile bundle config path ${{ env.BUNDLE_PATH }} From a4ad163c38e3e4555d4628f85d4514f604060ee0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 09:51:12 -0500 Subject: [PATCH 328/524] Use a Ruby version tested by solargraph-rspec --- .github/workflows/plugins.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 1d698f43a..6de580b2f 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -121,7 +121,8 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 + # solargraph-rspec's tests run against 3.1, 3.2 and 3.3 + ruby-version: '3.1' bundler-cache: false - name: Install gems run: | From c5df6965b99536d7dc0efde20e19ada24bcf34ed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 10:07:01 -0500 Subject: [PATCH 329/524] Add appraisal workaround --- .github/workflows/plugins.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 6de580b2f..24ca3f836 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -133,6 +133,14 @@ jobs: bundle config path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 bundle exec appraisal install + # @todo some kind of appraisal/bundle conflict? + # https://github.com/castwide/solargraph/actions/runs/19038710934/job/54369767122?pr=1116 + # /home/runner/work/solargraph/solargraph-rspec/vendor/bundle/ruby/3.1.0/gems/bundler-2.6.9/lib/bundler/runtime.rb:317:in + # `check_for_activated_spec!': You have already activated date + # 3.5.0, but your Gemfile requires date 3.4.1. Prepending + # `bundle exec` to your command may solve + # this. (Gem::LoadError) + bundle exec appraisal update date - name: Configure .solargraph.yml run: | cd ../solargraph-rspec From 7cefc1cabe4533f8265c18baa717bca422da75ed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 10:08:14 -0500 Subject: [PATCH 330/524] Add resgression spec --- spec/pin/base_variable_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 8c462bff3..2a49ac9f1 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -44,4 +44,19 @@ def bar expect(type.to_rbs).to eq('(1 | nil)') expect(type.simplify_literals.to_rbs).to eq('(::Integer | ::NilClass)') end + + it "understands proc kwarg parameters aren't affected by @type" do + code = %( + # @return [Proc] + def foo + # @type [Proc] + # @param layout [Boolean] + @render_method = proc { |layout = false| + 123 if layout + } + end + ) + checker = Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + expect(checker.problems.map(&:message)).to eq([]) + end end From cccd47a5174ea3ceabd612c60de8701efe62c164 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 20:58:40 -0500 Subject: [PATCH 331/524] Fix solargraph-rspec specs --- lib/solargraph/convention/data_definition.rb | 2 +- lib/solargraph/convention/struct_definition.rb | 3 ++- .../parser_gem/node_processors/block_node.rb | 17 ++++++++++++++--- lib/solargraph/parser/region.rb | 8 ++++++-- lib/solargraph/pin/base.rb | 3 +++ lib/solargraph/pin/block.rb | 13 +++++++++++-- lib/solargraph/pin/namespace.rb | 6 ++++++ lib/solargraph/source/chain.rb | 10 +++++----- .../source/chain/instance_variable.rb | 2 +- lib/solargraph/source_map/clip.rb | 2 +- spec/source/chain/instance_variable_spec.rb | 6 +++++- spec/source_map/clip_spec.rb | 6 +----- 12 files changed, 56 insertions(+), 22 deletions(-) diff --git a/lib/solargraph/convention/data_definition.rb b/lib/solargraph/convention/data_definition.rb index 8efe27932..90b5a64c3 100644 --- a/lib/solargraph/convention/data_definition.rb +++ b/lib/solargraph/convention/data_definition.rb @@ -71,7 +71,7 @@ def process comments: attribute_comments(attribute_node, attribute_name)) end - process_children region.update(closure: nspin, visibility: :public) + process_children region.update(closure: nspin, scope: :instance, visibility: :public) false end diff --git a/lib/solargraph/convention/struct_definition.rb b/lib/solargraph/convention/struct_definition.rb index b34ae5494..2da32c18d 100644 --- a/lib/solargraph/convention/struct_definition.rb +++ b/lib/solargraph/convention/struct_definition.rb @@ -96,7 +96,8 @@ def process end end - process_children region.update(closure: nspin, visibility: :public) + # the child here is the struct body, which uses Class as the binder + process_children region.update(closure: nspin, scope: :instance, visibility: :public) false end diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index c77713fad..3fb81ca33 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -13,14 +13,23 @@ def process scope = region.scope || region.closure.context.scope if other_class_eval? clazz_name = unpack_name(node.children[0].children[0]) - binder = ComplexType.try_parse("Class<#{clazz_name}>") + # instance variables should come from the Class type + context = ComplexType.try_parse("Class<#{clazz_name}>") + # when resolving method calls inside this block, we + # should be working inside Class, not T - that's the + # whole point of 'class_eval'. e.g., def foo defines it + # as an instance method in the class, like Foo.def + # instead of Foo.new.def + binder = context + scope = :class end + binder ||= region.binder block_pin = Solargraph::Pin::Block.new( location: location, closure: region.closure, node: node, - context: binder, + context: context, binder: binder, receiver: node.children[0], comments: comments_for(node), @@ -28,7 +37,9 @@ def process source: :parser ) pins.push block_pin - process_children region.update(closure: block_pin) + # A 'def' inside creates an instance method + child_scope = :instance + process_children region.update(closure: block_pin, scope: child_scope, binder: binder) end private diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index a6559bc8a..1844156fa 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -9,6 +9,8 @@ class Region # @return [Pin::Closure] attr_reader :closure + attr_reader :binder + # @return [Symbol] attr_reader :scope @@ -24,13 +26,14 @@ class Region # @param source [Source] # @param namespace [String] # @param closure [Pin::Closure, nil] + # @param binder [ComplexType, ComplexType::SimpleType, nil] # @param scope [Symbol, nil] # @param visibility [Symbol] # @param lvars [Array] def initialize source: Solargraph::Source.load_string(''), closure: nil, + binder: nil, scope: nil, visibility: :public, lvars: [] @source = source - # @closure = closure @closure = closure || Pin::Namespace.new(name: '', location: source.location, source: :parser) @scope = scope @visibility = visibility @@ -49,10 +52,11 @@ def filename # @param visibility [Symbol, nil] # @param lvars [Array, nil] # @return [Region] - def update closure: nil, scope: nil, visibility: nil, lvars: nil + def update closure: nil, binder: nil, scope: nil, visibility: nil, lvars: nil Region.new( source: source, closure: closure || self.closure, + binder: binder || closure&.binder || self.closure.binder, scope: scope || self.scope, visibility: visibility || self.visibility, lvars: lvars || self.lvars diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 7acf9d039..41f4f3a88 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -175,6 +175,9 @@ def reset_generated! # Same with @directives, @macros, @maybe_directives, which # regenerate docstring @deprecated = nil + @context = nil + @binder = nil + @path = nil reset_conversions end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 7a63ccc13..bd9cf8a52 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -21,8 +21,9 @@ def initialize receiver: nil, binder: nil, args: [], context: nil, node: nil, ** @receiver = receiver @context = context @return_type = ComplexType.parse('::Proc') - @rebind = binder if binder + @binder = binder if binder @node = node + @name = '' end # @param api_map [ApiMap] @@ -32,7 +33,14 @@ def rebind api_map end def binder - @rebind&.defined? ? @rebind : closure.binder + out = @rebind if @rebind&.defined? + out ||= @binder + out ||= closure.binder + end + + def context + @context = @rebind if @rebind&.defined? + super end # @param yield_types [::Array] @@ -89,6 +97,7 @@ def maybe_rebind api_map chain = Parser.chain(receiver, location.filename, node) locals = api_map.source_map(location.filename).locals_at(location) + closure.rebind api_map receiver_pin = chain.define(api_map, closure, locals).first return ComplexType::UNDEFINED unless receiver_pin diff --git a/lib/solargraph/pin/namespace.rb b/lib/solargraph/pin/namespace.rb index 95bd1089a..ca6cd6f84 100644 --- a/lib/solargraph/pin/namespace.rb +++ b/lib/solargraph/pin/namespace.rb @@ -48,6 +48,12 @@ def initialize type: :class, visibility: :public, gates: [''], name: '', **splat @name = name end + def reset_generated! + @return_type = nil + @full_context = nil + @path = nil + end + def to_rbs "#{@type.to_s} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index e941a347d..e8fef73f2 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -78,11 +78,11 @@ def base # # @param api_map [ApiMap] # - # @param name_pin [Pin::Base] A pin - # representing the place in which expression is evaluated (e.g., - # a Method pin, or a Module or Class pin if not run within a - # method - both in terms of the closure around the chain, as well - # as the self type used for any method calls in head position. + # @param name_pin [Pin::Base] A pin representing the closure in + # which expression is evaluated (e.g., a Method pin, or a + # Module or Class pin if not run within a method - both in + # terms of the closure around the chain, as well as the self + # type used for any method calls in head position. # # Requirements for name_pin: # diff --git a/lib/solargraph/source/chain/instance_variable.rb b/lib/solargraph/source/chain/instance_variable.rb index ea09f5578..08d71455c 100644 --- a/lib/solargraph/source/chain/instance_variable.rb +++ b/lib/solargraph/source/chain/instance_variable.rb @@ -5,7 +5,7 @@ class Source class Chain class InstanceVariable < Link def resolve api_map, name_pin, locals - api_map.get_instance_variable_pins(name_pin.binder.namespace, name_pin.binder.scope).select{|p| p.name == word} + api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select{|p| p.name == word} end end end diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index f7e76b711..904f81e93 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -199,7 +199,7 @@ def code_complete if cursor.word.start_with?('@@') return package_completions(api_map.get_class_variable_pins(context_pin.full_context.namespace)) elsif cursor.word.start_with?('@') - return package_completions(api_map.get_instance_variable_pins(closure.binder.namespace, closure.binder.scope)) + return package_completions(api_map.get_instance_variable_pins(closure.full_context.namespace, closure.context.scope)) elsif cursor.word.start_with?('$') return package_completions(api_map.get_global_variable_pins) end diff --git a/spec/source/chain/instance_variable_spec.rb b/spec/source/chain/instance_variable_spec.rb index 8326a66d2..4838faf8f 100644 --- a/spec/source/chain/instance_variable_spec.rb +++ b/spec/source/chain/instance_variable_spec.rb @@ -11,7 +11,11 @@ expect(pins.length).to eq(1) expect(pins.first.name).to eq('@foo') expect(pins.first.context.scope).to eq(:instance) - pins = link.resolve(api_map, closure, []) + # Lookup context is Class to find the civar + name_pin = Solargraph::Pin::ProxyType.anonymous(closure.binder, + # Closure is the class + closure: closure) + pins = link.resolve(api_map, name_pin, []) expect(pins.length).to eq(1) expect(pins.first.name).to eq('@foo') expect(pins.first.context.scope).to eq(:class) diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index c0a60fff0..dd865ebde 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -679,17 +679,13 @@ def initialize @foo._ end end - Foo.define_method(:test2) do - @foo._ - define_method(:test4) { @foo._ } # only handle Module#define_method, other pin is ignored.. - end Foo.class_eval do define_method(:test5) { @foo._ } end ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source - [[4, 39], [7, 15], [11, 13], [12, 37], [15, 37]].each do |loc| + [[4, 39], [7, 15], [11, 37]].each do |loc| clip = api_map.clip_at('test.rb', loc) paths = clip.complete.pins.map(&:path) expect(paths).to include('String#upcase'), -> { %(expected #{paths} at #{loc} to include "String#upcase") } From d8ef859cee55f233bb0c5c3a6d49238cd2b4f584 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 21:05:26 -0500 Subject: [PATCH 332/524] Fix RuboCop issues --- lib/solargraph/parser/region.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index 1844156fa..f700038b0 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -24,17 +24,16 @@ class Region attr_reader :lvars # @param source [Source] - # @param namespace [String] # @param closure [Pin::Closure, nil] - # @param binder [ComplexType, ComplexType::SimpleType, nil] + # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param scope [Symbol, nil] # @param visibility [Symbol] # @param lvars [Array] def initialize source: Solargraph::Source.load_string(''), closure: nil, - binder: nil, - scope: nil, visibility: :public, lvars: [] + binder: nil, scope: nil, visibility: :public, lvars: [] @source = source @closure = closure || Pin::Namespace.new(name: '', location: source.location, source: :parser) + @binder = binder @scope = scope @visibility = visibility @lvars = lvars @@ -48,6 +47,7 @@ def filename # Generate a new Region with the provided attribute changes. # # @param closure [Pin::Closure, nil] + # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] # @param lvars [Array, nil] From f7bc6241258eb632862f6dfe44313856c8e6495e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 21:26:10 -0500 Subject: [PATCH 333/524] Use dev branch for solargraph-rspec for now --- .github/workflows/plugins.yml | 6 +++++- lib/solargraph/pin/common.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 24ca3f836..a298a0eab 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -116,7 +116,11 @@ jobs: - name: clone https://github.com/lekemula/solargraph-rspec/ run: | cd .. - git clone https://github.com/lekemula/solargraph-rspec.git + # git clone https://github.com/lekemula/solargraph-rspec.git + + # pending https://github.com/lekemula/solargraph-rspec/pull/30 + git clone https://github.com/apiology/solargraph-rspec.git + git checkout reset_closures cd solargraph-rspec - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 062099ee4..4397a707c 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -11,6 +11,16 @@ module Common # @return [Location] attr_reader :location + def location=(value) + @location = value + reset_generated! + end + + def closure=(value) + @closure = value + reset_generated! + end + # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred # @return [Pin::Closure, nil] def closure From 461ab2d6af6e2c90c44586c64e60c2c6c10d6ce5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 21:31:39 -0500 Subject: [PATCH 334/524] Fix GHA --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a298a0eab..86fe788a1 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -120,8 +120,8 @@ jobs: # pending https://github.com/lekemula/solargraph-rspec/pull/30 git clone https://github.com/apiology/solargraph-rspec.git - git checkout reset_closures cd solargraph-rspec + git checkout reset_closures - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From b71a1841c6bd2390f7a20f930a2fd179579c1499 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 21:37:42 -0500 Subject: [PATCH 335/524] Fix linting issues --- lib/solargraph/pin/common.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 4397a707c..c37638c30 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -6,16 +6,23 @@ module Common # @!method source # @abstract # @return [Source, nil] + # @!method reset_generated! + # @abstract + # @return [void] # @type @closure [Pin::Closure, nil] # @return [Location] attr_reader :location + # @param value [Location] + # @return [void] def location=(value) @location = value reset_generated! end + # @param value [Pin::Closure] + # @return [void] def closure=(value) @closure = value reset_generated! From 9a4b8899da19204da2dd7e10b93e3cf2acc5eceb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 22:06:03 -0500 Subject: [PATCH 336/524] Add solargraph update --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 86fe788a1..a2434c9c1 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -152,6 +152,7 @@ jobs: - name: Solargraph generate RSpec gems YARD and RBS pins run: | cd ../solargraph-rspec + bundle exec appraisal rbs collection update rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) bundle exec appraisal solargraph gems $rspec_gems - name: Run specs From 069ce930fa29bd3a48c980d4da070857db841566 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 22:17:14 -0500 Subject: [PATCH 337/524] Fix appraisal in plugins.yml --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a2434c9c1..3970f95d3 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -153,7 +153,7 @@ jobs: run: | cd ../solargraph-rspec bundle exec appraisal rbs collection update - rspec_gems=$(bundle exec ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) + rspec_gems=$(bundle exec appraisal ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) bundle exec appraisal solargraph gems $rspec_gems - name: Run specs run: | From fae78ad0d2d2e8fb7fc49bce9d8510f82fc5f8f3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 11 Nov 2025 22:25:39 -0500 Subject: [PATCH 338/524] Catch Gem::Requirement::BadRequirementError --- lib/solargraph/shell.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..f193aba43 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -149,6 +149,10 @@ def gems *names do_cache spec, api_map rescue Gem::MissingSpecError warn "Gem '#{name}' not found" + rescue Gem::Requirement::BadRequirementError => e + warn "Gem '#{name}' failed while loading" + warn e.message + warn e.backtrace.join("\n") end STDERR.puts "Documentation cached for #{names.count} gems." end From 0d877a4c8e8f952e905ae647010a8e6cf8b89817 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Nov 2025 10:29:06 -0500 Subject: [PATCH 339/524] Bump Ruy versioin for solargraph-rspec --- .github/workflows/plugins.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 3970f95d3..b9aec8bdb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -125,8 +125,9 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - # solargraph-rspec's tests run against 3.1, 3.2 and 3.3 - ruby-version: '3.1' + # solargraph-rspec's tests run against 3.1, 3.2 and 3.3, but + # fail on 3.1 as of 2025-11-12 + ruby-version: '3.2' bundler-cache: false - name: Install gems run: | From f886fed4f0ec5d4df5013b110ae9074e81968627 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Nov 2025 10:34:20 -0500 Subject: [PATCH 340/524] Fix build failure via rubygems update --- .github/workflows/plugins.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b9aec8bdb..eaedc4bf5 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -125,9 +125,8 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - # solargraph-rspec's tests run against 3.1, 3.2 and 3.3, but - # fail on 3.1 as of 2025-11-12 - ruby-version: '3.2' + ruby-version: '3.1' + rubygems: latest bundler-cache: false - name: Install gems run: | From 331f56b1c17ad03d7e4174bb0087e3a350471ef7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Nov 2025 10:41:25 -0500 Subject: [PATCH 341/524] Bump rspec-rails just as solargraph-rails' GHA does --- .github/workflows/plugins.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index eaedc4bf5..fa704e2ae 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -145,6 +145,10 @@ jobs: # `bundle exec` to your command may solve # this. (Gem::LoadError) bundle exec appraisal update date + # For some reason on ruby 3.1 it defaults to an old version: 1.3.2 + # https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22 + # We update manually to the latest + bundle exec appraisal update rspec-rails - name: Configure .solargraph.yml run: | cd ../solargraph-rspec From 7021433b2419580baffa6a2f7e3a04829dc492f0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Nov 2025 17:13:28 -0500 Subject: [PATCH 342/524] Fix merge --- lib/solargraph/pin/base_variable.rb | 10 ++++------ lib/solargraph/pin/local_variable.rb | 9 --------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 433351c29..f40175e90 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -45,13 +45,11 @@ def combine_with(other, attrs={}) # @param other [self] # - # @return [Array(Parser::AST::Node, Integer), nil] - # - # @sg-ignore - # Solargraph::Pin::BaseVariable#combine_mass_assignment return - # type could not be inferred + # @return [Array(AST::Node, Integer), nil] def combine_mass_assignment(other) - assert_same(other, :mass_assignment) + # @todo pick first non-nil arbitrarily - we don't yet support + # mass assignment merging + mass_assignment || other.mass_assignment end # @return [Parser::AST::Node, nil] diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 72b85b6d5..4b97feb21 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -96,15 +96,6 @@ def combine_return_type(other) combine_types(other, :return_type) end - # @param other [self] - # - # @return [Array(AST::Node, Integer), nil] - def combine_mass_assignment(other) - # @todo pick first non-nil arbitrarily - we don't yet support - # mass assignment merging - mass_assignment || other.mass_assignment - end - def probe api_map if presence_certain? && return_type&.defined? # flow sensitive typing has already probed this type - use From 3161f917b1103d7f2241348e6e3e2ee4dc60d83f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 12 Nov 2025 17:26:52 -0500 Subject: [PATCH 343/524] Clarify expectation of instance variable lookup from context, but method lookup from binder --- lib/solargraph/source/chain.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index e8fef73f2..e24b11c92 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -87,16 +87,16 @@ def base # Requirements for name_pin: # # * name_pin.context: This should be a type representing the - # namespace where we can look up non-local variables and - # method names. If it is a Class, we will look up - # :class scoped methods/variables. + # namespace where we can look up non-local variables. If + # it is a Class, we will look up :class scoped + # instance variables. # # * name_pin.binder: Used for method call lookups only # (Chain::Call links). For method calls as the first # element in the chain, 'name_pin.binder' should be the # same as name_pin.context above. For method calls later - # in the chain (e.g., 'b' in a.b.c), it should represent - # 'a'. + # in the chain, it changes. (e.g., for 'b' in a.b.c, it + # should represent the type of 'a'). # # @param locals [::Array] Any local # variables / method parameters etc visible by the statement From a3fc979de374b9b1b5836dfe9fe83c06bfafce32 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 13 Nov 2025 08:30:48 -0500 Subject: [PATCH 344/524] Remove unneeded changes --- lib/solargraph/convention/data_definition.rb | 2 +- .../convention/struct_definition.rb | 3 +- .../parser_gem/node_processors/block_node.rb | 7 +--- .../parser_gem/node_processors/def_node.rb | 3 ++ lib/solargraph/parser/region.rb | 10 +---- lib/solargraph/pin/base.rb | 4 -- lib/solargraph/pin/base_variable.rb | 42 +++++++------------ lib/solargraph/pin/common.rb | 14 +++---- 8 files changed, 29 insertions(+), 56 deletions(-) diff --git a/lib/solargraph/convention/data_definition.rb b/lib/solargraph/convention/data_definition.rb index 90b5a64c3..8efe27932 100644 --- a/lib/solargraph/convention/data_definition.rb +++ b/lib/solargraph/convention/data_definition.rb @@ -71,7 +71,7 @@ def process comments: attribute_comments(attribute_node, attribute_name)) end - process_children region.update(closure: nspin, scope: :instance, visibility: :public) + process_children region.update(closure: nspin, visibility: :public) false end diff --git a/lib/solargraph/convention/struct_definition.rb b/lib/solargraph/convention/struct_definition.rb index 2da32c18d..b34ae5494 100644 --- a/lib/solargraph/convention/struct_definition.rb +++ b/lib/solargraph/convention/struct_definition.rb @@ -96,8 +96,7 @@ def process end end - # the child here is the struct body, which uses Class as the binder - process_children region.update(closure: nspin, scope: :instance, visibility: :public) + process_children region.update(closure: nspin, visibility: :public) false end diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index 3fb81ca33..f4c0ff430 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -14,6 +14,7 @@ def process if other_class_eval? clazz_name = unpack_name(node.children[0].children[0]) # instance variables should come from the Class type + # - i.e., treated as class instance variables context = ComplexType.try_parse("Class<#{clazz_name}>") # when resolving method calls inside this block, we # should be working inside Class, not T - that's the @@ -21,10 +22,8 @@ def process # as an instance method in the class, like Foo.def # instead of Foo.new.def binder = context - scope = :class end - binder ||= region.binder block_pin = Solargraph::Pin::Block.new( location: location, closure: region.closure, @@ -37,9 +36,7 @@ def process source: :parser ) pins.push block_pin - # A 'def' inside creates an instance method - child_scope = :instance - process_children region.update(closure: block_pin, scope: child_scope, binder: binder) + process_children region.update(closure: block_pin) end private diff --git a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb index c4e4f9a70..1b9fd442d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/def_node.rb @@ -8,6 +8,9 @@ class DefNode < Parser::NodeProcessor::Base def process name = node.children[0].to_s scope = region.scope || (region.closure.is_a?(Pin::Singleton) ? :class : :instance) + # specify context explicitly instead of relying on + # closure, as they may differ (e.g., defs inside + # class_eval) method_context = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder methpin = Solargraph::Pin::Method.new( location: get_node_location(node), diff --git a/lib/solargraph/parser/region.rb b/lib/solargraph/parser/region.rb index f700038b0..279ad0e57 100644 --- a/lib/solargraph/parser/region.rb +++ b/lib/solargraph/parser/region.rb @@ -9,8 +9,6 @@ class Region # @return [Pin::Closure] attr_reader :closure - attr_reader :binder - # @return [Symbol] attr_reader :scope @@ -25,15 +23,13 @@ class Region # @param source [Source] # @param closure [Pin::Closure, nil] - # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param scope [Symbol, nil] # @param visibility [Symbol] # @param lvars [Array] def initialize source: Solargraph::Source.load_string(''), closure: nil, - binder: nil, scope: nil, visibility: :public, lvars: [] + scope: nil, visibility: :public, lvars: [] @source = source @closure = closure || Pin::Namespace.new(name: '', location: source.location, source: :parser) - @binder = binder @scope = scope @visibility = visibility @lvars = lvars @@ -47,16 +43,14 @@ def filename # Generate a new Region with the provided attribute changes. # # @param closure [Pin::Closure, nil] - # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] # @param lvars [Array, nil] # @return [Region] - def update closure: nil, binder: nil, scope: nil, visibility: nil, lvars: nil + def update closure: nil, scope: nil, visibility: nil, lvars: nil Region.new( source: source, closure: closure || self.closure, - binder: binder || closure&.binder || self.closure.binder, scope: scope || self.scope, visibility: visibility || self.visibility, lvars: lvars || self.lvars diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 41f4f3a88..38773ebfe 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -175,9 +175,6 @@ def reset_generated! # Same with @directives, @macros, @maybe_directives, which # regenerate docstring @deprecated = nil - @context = nil - @binder = nil - @path = nil reset_conversions end @@ -579,7 +576,6 @@ def proxy return_type result = dup result.return_type = return_type result.proxied = true - result.reset_generated! result end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index f40175e90..21f8bf3e2 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -131,7 +131,7 @@ def probe api_map type.all_params.first end end.compact! - return ComplexType.new(types.uniq).qualify(api_map, *gates) unless types.empty? + return ComplexType.new(types.uniq) unless types.empty? end ComplexType::UNDEFINED @@ -149,40 +149,28 @@ def type_desc private - # See if this variable is visible within 'other_closure' + # See if this variable is visible within 'viewing_closure' # - # @param other_closure [Pin::Closure] + # @param viewing_closure [Pin::Closure] # @return [Boolean] - def visible_in_closure? other_closure - needle = closure - haystack = other_closure + def visible_in_closure? viewing_closure + # if we're declared at top level, we can't be seen from within + # methods declared tere + return false if viewing_closure.is_a?(Pin::Method) && closure.context.tags == 'Class<>' - cursor = haystack + return true if viewing_closure.binder.namespace == closure.binder.namespace - until cursor.nil? - if cursor.is_a?(Pin::Method) && closure.context.tags == 'Class<>' - # methods can't see local variables declared in their - # parent closure - return false - end + return true if viewing_closure.return_type == closure.context - if cursor.binder.namespace == needle.binder.namespace - return true - end + # classes and modules can't see local variables declared + # in their parent closure, so stop here + return false if scope == :instance && viewing_closure.is_a?(Pin::Namespace) - if cursor.return_type == needle.context - return true - end + parent_of_viewing_closure = viewing_closure.closure - if scope == :instance && cursor.is_a?(Pin::Namespace) - # classes and modules can't see local variables declared - # in their parent closure, so stop here - return false - end + return false if parent_of_viewing_closure.nil? - cursor = cursor.closure - end - false + visible_in_closure?(parent_of_viewing_closure) end # @param other [self] diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index c37638c30..90bb434cd 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -12,20 +12,16 @@ module Common # @type @closure [Pin::Closure, nil] # @return [Location] - attr_reader :location - - # @param value [Location] - # @return [void] - def location=(value) - @location = value - reset_generated! - end + attr_accessor :location # @param value [Pin::Closure] # @return [void] def closure=(value) @closure = value - reset_generated! + # remove cached values generated from closure + @context = nil + @binder = nil + @path = nil end # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred From 54b65b8a8f0cbb05a2dd499da4bb8ee0ead5cc66 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:37:09 -0500 Subject: [PATCH 345/524] Drop Block's binder param --- .../parser/parser_gem/node_processors/block_node.rb | 7 ------- lib/solargraph/pin/block.rb | 3 +-- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index f4c0ff430..0fb787eb1 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -16,12 +16,6 @@ def process # instance variables should come from the Class type # - i.e., treated as class instance variables context = ComplexType.try_parse("Class<#{clazz_name}>") - # when resolving method calls inside this block, we - # should be working inside Class, not T - that's the - # whole point of 'class_eval'. e.g., def foo defines it - # as an instance method in the class, like Foo.def - # instead of Foo.new.def - binder = context scope = :class end block_pin = Solargraph::Pin::Block.new( @@ -29,7 +23,6 @@ def process closure: region.closure, node: node, context: context, - binder: binder, receiver: node.children[0], comments: comments_for(node), scope: scope, diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index bd9cf8a52..951486d5b 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -34,8 +34,7 @@ def rebind api_map def binder out = @rebind if @rebind&.defined? - out ||= @binder - out ||= closure.binder + out ||= super end def context From dc5237b12139090d1939c1ee784d099ace020277 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:39:26 -0500 Subject: [PATCH 346/524] Drop Method's binder param --- lib/solargraph/pin/method.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 94d254ddc..f93dd06d2 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -23,10 +23,8 @@ class Method < Callable # @param signatures [::Array, nil] # @param anon_splat [Boolean] # @param context [ComplexType, nil] - # @param binder [ComplexType, nil] def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, - context: nil, binder: nil, - **splat + context: nil, **splat super(**splat) @visibility = visibility @explicit = explicit From cdc6de74c93d7e580ee6876aa86136400765300e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:40:38 -0500 Subject: [PATCH 347/524] Drop binder --- lib/solargraph/parser/parser_gem/node_processors/block_node.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb index 0fb787eb1..e653fd95e 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/block_node.rb @@ -9,7 +9,6 @@ class BlockNode < Parser::NodeProcessor::Base def process location = get_node_location(node) - binder = nil scope = region.scope || region.closure.context.scope if other_class_eval? clazz_name = unpack_name(node.children[0].children[0]) From f2db36519c7535c1df773da6ae782c4e1c578111 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:44:33 -0500 Subject: [PATCH 348/524] Drop binder --- lib/solargraph/pin/block.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 951486d5b..8f36589f9 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -12,16 +12,14 @@ class Block < Callable attr_reader :node # @param receiver [Parser::AST::Node, nil] - # @param binder [Pin::Namespace, nil] # @param node [Parser::AST::Node, nil] # @param context [ComplexType, nil] # @param args [::Array] - def initialize receiver: nil, binder: nil, args: [], context: nil, node: nil, **splat + def initialize receiver: nil, args: [], context: nil, node: nil, **splat super(**splat, parameters: args) @receiver = receiver @context = context @return_type = ComplexType.parse('::Proc') - @binder = binder if binder @node = node @name = '' end From 786375e3c1072a27c29ebcb817f718b0e04be856 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:47:42 -0500 Subject: [PATCH 349/524] Drop closure rebinding --- lib/solargraph/pin/block.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 8f36589f9..75cb20fa2 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -94,7 +94,6 @@ def maybe_rebind api_map chain = Parser.chain(receiver, location.filename, node) locals = api_map.source_map(location.filename).locals_at(location) - closure.rebind api_map receiver_pin = chain.define(api_map, closure, locals).first return ComplexType::UNDEFINED unless receiver_pin From 17b120b30a824073419a00c5a7cc2f297d308016 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:53:50 -0500 Subject: [PATCH 350/524] Drop reset_generated! --- lib/solargraph/pin/common.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 90bb434cd..9cee855f9 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -6,9 +6,6 @@ module Common # @!method source # @abstract # @return [Source, nil] - # @!method reset_generated! - # @abstract - # @return [void] # @type @closure [Pin::Closure, nil] # @return [Location] From 62a447c70a9fbf2bf6fe9ecd8b157332d132f7fe Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 08:58:19 -0500 Subject: [PATCH 351/524] Drop binder --- lib/solargraph/pin/method.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index f93dd06d2..1cb506951 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -34,7 +34,6 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @signatures = signatures @anon_splat = anon_splat @context = context if context - @binder = binder if binder end # @return [Array] From 379daf23f163268f31cdfd324f5570f02618fa58 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 09:03:54 -0500 Subject: [PATCH 352/524] Document logic --- 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 2abda62d8..5ed82f408 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -362,6 +362,8 @@ def var_at_location(locals, name, closure, location) with_correct_name = locals.select { |pin| pin.name == name} with_presence = with_correct_name.reject { |pin| pin.presence.nil? } vars_at_location = with_presence.reject do |pin| + # visible_at? excludes the starting position, but we want to + # include it for this purpose (!pin.visible_at?(closure, location) && !pin.starts_at?(location)) end From fc4f00fb047ede2561afe01a5c0616644c4c94a6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 09:11:39 -0500 Subject: [PATCH 353/524] Use reset_generated! instead, so future cases are handled --- lib/solargraph/pin/common.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 9cee855f9..04a886aa0 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -6,6 +6,9 @@ module Common # @!method source # @abstract # @return [Source, nil] + # @!method reset_generated! + # @abstract + # @return [void] # @type @closure [Pin::Closure, nil] # @return [Location] @@ -16,9 +19,7 @@ module Common def closure=(value) @closure = value # remove cached values generated from closure - @context = nil - @binder = nil - @path = nil + reset_generated! end # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred From 7be2cd10a59ddf98e523a46507014f18705a8cd6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 09:18:46 -0500 Subject: [PATCH 354/524] Revert refactor --- lib/solargraph/source/chain/call.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 0688643b4..d7a1b492a 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -94,10 +94,7 @@ def inferred_pins pins, api_map, name_pin, locals match = ol.parameters.any?(&:restarg?) break end - arg_name_pin = Pin::ProxyType.anonymous(name_pin.context, - closure: name_pin.closure, - source: :chain) - atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals) + atype = atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(name_pin.context, source: :chain), locals) unless param.compatible_arg?(atype, api_map) || param.restarg? match = false break From 401b3e6b5e443df537ecc69834e8b511b959b33a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 14 Nov 2025 09:20:49 -0500 Subject: [PATCH 355/524] Reformat --- lib/solargraph/source/chain/call.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index d7a1b492a..1294e8136 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -321,8 +321,8 @@ def block_call_type(api_map, name_pin, locals) block_pin = find_block_pin(api_map) # We use the block pin as the closure, as the parameters - # here will only be defined inside the block itself and we need to be able to see them - + # here will only be defined inside the block itself and we + # need to be able to see them block.infer(api_map, block_pin, locals) end end From a7cc436a2338d4eef00d9f92119535612846813a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 09:14:18 -0500 Subject: [PATCH 356/524] Fix solargraph-rspec specs --- lib/solargraph/pin/base.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 38773ebfe..e8dc16780 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -175,6 +175,9 @@ def reset_generated! # Same with @directives, @macros, @maybe_directives, which # regenerate docstring @deprecated = nil + @context = nil + @binder = nil + @path = nil reset_conversions end From fd463fe5df33f71e5d86422eb6c1b39801550fa6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 09:50:04 -0500 Subject: [PATCH 357/524] Try rubygems: latest --- .github/workflows/plugins.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..0e5398eb9 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -55,6 +55,8 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' + # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 + rubygems: latest bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: From 8bbd6f56307a0597546ee07a3a2848031d31580d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 09:54:47 -0500 Subject: [PATCH 358/524] Fix new Ruby 4.x issue --- .github/workflows/rspec.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..8bf12c47f 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,6 +35,8 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 + bundler: Gemfile.lock bundler-cache: false - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile From cd2fd9a62fc529da64295daee0de2a0561e143d5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 09:56:47 -0500 Subject: [PATCH 359/524] Fix new Ruby 4.x issue --- .github/workflows/rspec.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 8bf12c47f..b213c5ed0 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -36,7 +36,9 @@ jobs: with: ruby-version: ${{ matrix.ruby-version }} # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 - bundler: Gemfile.lock + # + # match version in Gemfile.lock and use same version below + bundler: 2.5.23 bundler-cache: false - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile @@ -48,7 +50,7 @@ jobs: run: echo "gem 'tsort'" >> .Gemfile - name: Install gems run: | - bundle install + bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version - name: Run tests run: bundle exec rake spec From ce65d5fa283312fa24f4c7d3175a4188a961b587 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 10:34:14 -0500 Subject: [PATCH 360/524] Separate out possible bug reproduction from original purpose of spec --- spec/convention/gemfile_spec.rb | 4 ++-- spec/type_checker/levels/strict_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/spec/convention/gemfile_spec.rb b/spec/convention/gemfile_spec.rb index 827da7993..ab6a5ef1b 100644 --- a/spec/convention/gemfile_spec.rb +++ b/spec/convention/gemfile_spec.rb @@ -24,7 +24,7 @@ def type_checker code it 'finds bad arguments to DSL methods' do checker = type_checker(%( - source File + source 123 gemspec bad_name: 'solargraph' @@ -35,7 +35,7 @@ def type_checker code expect(checker.problems.map(&:message).sort) .to eq(['Unrecognized keyword argument bad_name to Bundler::Dsl#gemspec', - 'Wrong argument type for Bundler::Dsl#source: source expected String, received Class'].sort) + 'Wrong argument type for Bundler::Dsl#source: source expected String, received 123'].sort) end it 'finds bad arguments to DSL ruby method' do diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index 47bf45a2c..38b267780 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -5,6 +5,18 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strict) end + it 'understands Class is not the same as String' do + checker = type_checker(%( + # @param str [String] + # @return [void] + def foo str; end + + foo File + )) + expect(checker.problems.map(&:message)) + .to eq(['Wrong argument type for #foo: str expected String, received Class']) + end + it 'handles compatible interfaces with self types on call' do checker = type_checker(%( # @param a [Enumerable] From c6dbe4d0899255deaa596f08b92494f90ef2886d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 11:19:09 -0500 Subject: [PATCH 361/524] Trigger build --- .github/workflows/plugins.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 2619b9ff8..b5984f3cb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -143,7 +143,6 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - # solargraph-rails supports Ruby 3.0+ ruby-version: '3.0' bundler-cache: false From 9d2686301d53487871584ab3cbb690c256e44114 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 11:22:22 -0500 Subject: [PATCH 362/524] Fix new bundler issue --- .github/workflows/rspec.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..b213c5ed0 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,6 +35,10 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 + # + # match version in Gemfile.lock and use same version below + bundler: 2.5.23 bundler-cache: false - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile @@ -46,7 +50,7 @@ jobs: run: echo "gem 'tsort'" >> .Gemfile - name: Install gems run: | - bundle install + bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version - name: Run tests run: bundle exec rake spec From 87ec9b35adfa75103bb0702282874a806ef7c054 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 11:42:41 -0500 Subject: [PATCH 363/524] Debug --- .github/workflows/rspec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index b213c5ed0..073594aac 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -52,6 +52,7 @@ jobs: run: | bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version + bundle list - name: Run tests run: bundle exec rake spec undercover: From 6a14bd7e09899beaaa9ee0370a42366ee81596f2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 12:28:38 -0500 Subject: [PATCH 364/524] Turn off warning diagnostics in CLI --- bin/solargraph | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/solargraph b/bin/solargraph index d85561700..248dc42fd 100755 --- a/bin/solargraph +++ b/bin/solargraph @@ -1,5 +1,8 @@ #!/usr/bin/env ruby +# turn off warning diagnostics from Ruby +$VERBOSE=nil + require 'solargraph' Solargraph::Shell.start(ARGV) From 7e7604bdfc5d76af86c5b7f5b25ade3d2201b52e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 13:38:08 -0500 Subject: [PATCH 365/524] Debug --- .github/workflows/rspec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 7b8ba9bfb..2743ca84f 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -61,6 +61,7 @@ jobs: bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version bundle list + bundle exec solargraph pin 'Bundler::Dsl#source' - name: Update types run: | bundle exec rbs collection update From 91d3cd4a981dcf651d1fa45ad78b96e2dd8bedbf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 18:12:51 -0500 Subject: [PATCH 366/524] Fix pin merging bug --- lib/solargraph/pin/base_variable.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 764c1fb39..fbf22cbaa 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -12,8 +12,9 @@ class BaseVariable < Base attr_accessor :mass_assignment # @param return_type [ComplexType, nil] + # @param mass_assignment [::Array(Parser::AST::Node, Integer), nil] # @param assignment [Parser::AST::Node, nil] - def initialize assignment: nil, return_type: nil, **splat + def initialize assignment: nil, return_type: nil, mass_assignment: nil, **splat super(**splat) @assignment = assignment # @type [nil, ::Array(Parser::AST::Node, Integer)] @@ -22,12 +23,12 @@ def initialize assignment: nil, return_type: nil, **splat end def combine_with(other, attrs={}) - attrs.merge({ + new_attrs = attrs.merge({ assignment: assert_same(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), }) - super(other, attrs) + super(other, new_attrs) end def completion_item_kind From 652fcd218c0532455baf88c0f05295f862f3dbf3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 18:32:44 -0500 Subject: [PATCH 367/524] Add @sg-ignore --- lib/solargraph/pin/base_variable.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index fbf22cbaa..da1f15cb2 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -28,6 +28,7 @@ def combine_with(other, attrs={}) mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), }) + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end From 4f94ef8c2a25f5fc80a7f38e69915022935aca24 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 22:48:36 -0500 Subject: [PATCH 368/524] Fix issue revealed by this PR's change --- lib/solargraph/pin/base_variable.rb | 5 +---- lib/solargraph/pin/local_variable.rb | 3 --- lib/solargraph/pin/parameter.rb | 1 - lib/solargraph/source/chain.rb | 11 +++++------ spec/type_checker/levels/strong_spec.rb | 24 ++++++++++++++++++++++-- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 21f8bf3e2..5d7f78620 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -31,15 +31,12 @@ def reset_generated! end def combine_with(other, attrs={}) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 new_assignments = combine_assignments(other) new_attrs = attrs.merge({ assignments: new_assignments, - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 mass_assignment: combine_mass_assignment(other), return_type: combine_return_type(other), - }) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 + }) super(other, new_attrs) end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 4b97feb21..a4c190261 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -23,12 +23,9 @@ def combine_with(other, attrs={}) # keep this as a parameter return other.combine_with(self, attrs) if other.is_a?(Parameter) && !self.is_a?(Parameter) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 new_assignments = combine_assignments(other) new_attrs = attrs.merge({ - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 presence: combine_presence(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 presence_certain: combine_presence_certain(other), }) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 4018d1201..af02a2f1c 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -42,7 +42,6 @@ def combine_with(other, attrs={}) asgn_code: asgn_code } end - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs.merge(attrs)) end diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index e24b11c92..db89f271f 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -210,11 +210,11 @@ def to_s private # @param pins [::Array] - # @param context [Pin::Base] + # @param name_pin [Pin::Base] # @param api_map [ApiMap] # @param locals [::Enumerable] # @return [ComplexType] - def infer_from_definitions pins, context, api_map, locals + def infer_from_definitions pins, name_pin, api_map, locals # @type [::Array] types = [] unresolved_pins = [] @@ -233,7 +233,7 @@ def infer_from_definitions pins, context, api_map, locals # @todo even at strong, no typechecking complaint # happens when a [Pin::Base,nil] is passed into a method # that accepts only [Pin::Namespace] as an argument - type = type.resolve_generics(pin.closure, context.binder) + type = type.resolve_generics(pin.closure, name_pin.binder) end types << type else @@ -270,12 +270,11 @@ def infer_from_definitions pins, context, api_map, locals else ComplexType.new(types) end - if context.nil? || context.return_type.undefined? + if name_pin.nil? || name_pin.context.undefined? # up to downstream to resolve self type return type end - - type.self_to_type(context.return_type) + type.self_to_type(name_pin.context) end # @param type [ComplexType] diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 1f0812fe4..85408191f 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -234,8 +234,6 @@ def bar end it 'understands complex use of self' do - pending 'https://github.com/castwide/solargraph/pull/1050' - checker = type_checker(%( class A # @param other [self] @@ -679,5 +677,27 @@ def baz(foo) expect(checker.problems.map(&:location).map(&:range).map(&:start)).to be_empty end + + it 'resolves self correctly in chained method calls' do + checker = type_checker(%( + class Foo + # @param other [self] + # + # @return [Symbol, nil] + def bar(other) + # @type [Symbol, nil] + baz(other) + end + + # @param other [self] + # + # @sg-ignore Missing @return tag + # @return [undefined] + def baz(other); end + end + )) + + expect(checker.problems.map(&:message)).to be_empty + end end end From ec29b25fd0f833ae778b750b9d9517c2726e9ba1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 00:13:45 -0500 Subject: [PATCH 369/524] Drop dead code --- lib/solargraph/pin/base_variable.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 16de9b7db..1a3488346 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -61,11 +61,6 @@ def combine_with(other, attrs={}) super(other, new_attrs) end - def reset_generated! - @return_type_minus_exclusions = nil - super - end - def inner_desc super + ", intersection_return_type=#{intersection_return_type&.rooted_tags.inspect}, exclude_return_type=#{exclude_return_type&.rooted_tags.inspect}" end From 963c13538f2e05650610d345a5df7dbd8d2a07e5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 11:30:28 -0500 Subject: [PATCH 370/524] Remove unused file This was replaced by yard_map/mapper/to_method.rb --- lib/solargraph/yard_map/to_method.rb | 88 ---------------------------- 1 file changed, 88 deletions(-) delete mode 100644 lib/solargraph/yard_map/to_method.rb diff --git a/lib/solargraph/yard_map/to_method.rb b/lib/solargraph/yard_map/to_method.rb deleted file mode 100644 index 3ecb7ac26..000000000 --- a/lib/solargraph/yard_map/to_method.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class YardMap - class ToMethod - module InnerMethods - module_function - - # @param code_object [YARD::CodeObjects::Base] - # @param location [Solargraph::Location] - # @param comments [String] - # @return [Array] - def get_parameters code_object, location, comments - return [] unless code_object.is_a?(YARD::CodeObjects::MethodObject) - # HACK: Skip `nil` and `self` parameters that are sometimes emitted - # for methods defined in C - # See https://github.com/castwide/solargraph/issues/345 - code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a| - Solargraph::Pin::Parameter.new( - location: location, - closure: self, - comments: comments, - name: arg_name(a), - presence: nil, - decl: arg_type(a), - asgn_code: a[1], - source: :yard_map - ) - end - end - - # @param a [Array] - # @return [String] - def arg_name a - a[0].gsub(/[^a-z0-9_]/i, '') - end - - # @param a [Array] - # @return [::Symbol] - def arg_type a - if a[0].start_with?('**') - :kwrestarg - elsif a[0].start_with?('*') - :restarg - elsif a[0].start_with?('&') - :blockarg - elsif a[0].end_with?(':') - a[1] ? :kwoptarg : :kwarg - elsif a[1] - :optarg - else - :arg - end - end - end - private_constant :InnerMethods - - include Helpers - - # @param code_object [YARD::CodeObjects::Base] - # @param name [String, nil] - # @param scope [Symbol, nil] - # @param visibility [Symbol, nil] - # @param closure [Solargraph::Pin::Base, nil] - # @param spec [Solargraph::Pin::Base, nil] - # @return [Solargraph::Pin::Method] - def 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] - ) - location = object_location(code_object, spec) - comments = code_object.docstring ? code_object.docstring.all.to_s : '' - Pin::Method.new( - location: location, - closure: closure, - name: name || code_object.name.to_s, - comments: comments, - scope: scope || code_object.scope, - visibility: visibility || code_object.visibility, - parameters: InnerMethods.get_parameters(code_object, location, comments), - explicit: code_object.is_explicit?, - source: :yard_map - ) - end - end - end -end From a12d959704c7151323ed022f4f6e3dcd957aa9c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 12:46:06 -0500 Subject: [PATCH 371/524] Add failing spec --- spec/type_checker/levels/alpha_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 781907d33..253efb3d3 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -73,5 +73,25 @@ def catalog expect(checker.problems.map(&:message)).to eq([]) end + + it 'can infer types based on || and &&' do + checker = type_checker(%( + class Baz + # @param bar [String, nil] + # @return [Boolean, String] + def foo bar + !bar || bar.upcase + end + + # @param bar [String, nil] + # @return [String, nil] + def bing bar + bar && bar.upcase + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end From 71f0e746174510663f2725aaa2e266b9157b6da5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 12:55:03 -0500 Subject: [PATCH 372/524] Add implementation --- .../parser/parser_gem/node_processors.rb | 4 +++- .../parser_gem/node_processors/or_node.rb | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 lib/solargraph/parser/parser_gem/node_processors/or_node.rb diff --git a/lib/solargraph/parser/parser_gem/node_processors.rb b/lib/solargraph/parser/parser_gem/node_processors.rb index e80970344..5f1634bba 100644 --- a/lib/solargraph/parser/parser_gem/node_processors.rb +++ b/lib/solargraph/parser/parser_gem/node_processors.rb @@ -30,6 +30,7 @@ module NodeProcessors autoload :WhenNode, 'solargraph/parser/parser_gem/node_processors/when_node' autoload :WhileNode, 'solargraph/parser/parser_gem/node_processors/while_node' autoload :AndNode, 'solargraph/parser/parser_gem/node_processors/and_node' + autoload :OrNode, 'solargraph/parser/parser_gem/node_processors/or_node' end end @@ -64,9 +65,10 @@ module NodeProcessor register :op_asgn, ParserGem::NodeProcessors::OpasgnNode register :sym, ParserGem::NodeProcessors::SymNode register :until, ParserGem::NodeProcessors::UntilNode + register :when, ParserGem::NodeProcessors::WhenNode register :while, ParserGem::NodeProcessors::WhileNode register :and, ParserGem::NodeProcessors::AndNode - register :when, ParserGem::NodeProcessors::WhenNode + register :or, ParserGem::NodeProcessors::OrNode end end end diff --git a/lib/solargraph/parser/parser_gem/node_processors/or_node.rb b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb new file mode 100644 index 000000000..c77bad1d6 --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class OrNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + process_children + + FlowSensitiveTyping.new(locals, + enclosing_breakable_pin, + enclosing_compound_statement_pin).process_or(node) + end + end + end + end + end +end From a7b7833dd015ff965875658c2c843121fd1af3b5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 14:09:11 -0500 Subject: [PATCH 373/524] Fix @sg-ignore issues --- lib/solargraph/pin/base_variable.rb | 5 +---- lib/solargraph/shell.rb | 15 --------------- lib/solargraph/source/chain/or.rb | 1 + 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index c5010dd20..cad335041 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -76,12 +76,9 @@ def combine_with(other, attrs={}) assignments: new_assignments, mass_assignment: combine_mass_assignment(other), return_type: combine_return_type(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 intersection_return_type: combine_types(other, :intersection_return_type), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 exclude_return_type: combine_types(other, :exclude_return_type), }) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end @@ -252,7 +249,7 @@ def combine_closure(other) end # if filenames are different, this will just pick one - # @sg-ignore flow sensitive typing needs to handle ivars + # @todo flow sensitive typing needs to handle ivars return closure if closure.location <= other.closure.location other.closure diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 1802ba746..eb4e65175 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -176,7 +176,6 @@ def reporters # @return [void] def typecheck *files directory = File.realpath(options[:directory]) - # @sg-ignore Unresolved call to options level = options[:level].to_sym rules = Solargraph::TypeChecker::Rules.new(level) api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout, loose_unions: rules.loose_unions?) @@ -189,7 +188,6 @@ def typecheck *files filecount = 0 time = Benchmark.measure { files.each do |file| - # @sg-ignore Unresolved call to options checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: options[:level].to_sym) problems = checker.problems next if problems.empty? @@ -248,24 +246,17 @@ def list puts "#{workspace.filenames.length} files total." end - # @sg-ignore Unresolved call to desc desc 'pin [PATH]', 'Describe a pin', hide: true - # @sg-ignore Unresolved call to option option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false - # @sg-ignore Unresolved call to option option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false - # @sg-ignore Unresolved call to option option :references, type: :boolean, desc: 'Show references', default: false - # @sg-ignore Unresolved call to option option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false - # @sg-ignore Unresolved call to option option :stack, type: :boolean, desc: 'Show entire stack of a method pin by including definitions in superclasses', default: false # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' # @return [void] def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) is_method = path.include?('#') || path.include?('.') - # @sg-ignore Unresolved call to options if is_method && options[:stack] scope, ns, meth = if path.include? '#' [:instance, *path.split('#', 2)] @@ -287,7 +278,6 @@ def pin path $stderr.puts "Pin not found for path '#{path}'" exit 1 when Pin::Namespace - # @sg-ignore Unresolved call to options if options[:references] superclass_tag = api_map.qualify_superclass(pin.return_type.tag) superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag @@ -296,12 +286,9 @@ def pin path end pins.each do |pin| - # @sg-ignore Unresolved call to options if options[:typify] || options[:probe] type = ComplexType::UNDEFINED - # @sg-ignore Unresolved call to options type = pin.typify(api_map) if options[:typify] - # @sg-ignore Unresolved call to options type = pin.probe(api_map) if options[:probe] && type.undefined? print_type(type) next @@ -345,7 +332,6 @@ def do_cache gemspec, api_map # @param type [ComplexType] # @return [void] def print_type(type) - # @sg-ignore Unresolved call to options if options[:rbs] puts type.to_rbs else @@ -356,7 +342,6 @@ def print_type(type) # @param pin [Solargraph::Pin::Base] # @return [void] def print_pin(pin) - # @sg-ignore Unresolved call to options if options[:rbs] puts pin.to_rbs else diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index fd56b52c0..884b06fd1 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -17,6 +17,7 @@ def resolve api_map, name_pin, locals types = @links.map { |link| link.infer(api_map, name_pin, locals) } combined_type = Solargraph::ComplexType.new(types) unless types.all?(&:nullable?) + # @sg-ignore Unresolved call to without_nil on Solargraph::ComplexType combined_type = combined_type.without_nil end From b524eec6a291ee9bed1ab0a9eceb7b43653cf813 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 14:12:32 -0500 Subject: [PATCH 374/524] Fix solargraph-rails specs issue --- .github/workflows/plugins.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 29d5616cc..5f294259d 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -181,6 +181,8 @@ jobs: ruby-version: '3.0' bundler-cache: false bundler: latest + # https://github.com/castwide/solargraph/actions/runs/19410641794/job/55531497679?pr=1123 + rubygems: latest env: MATRIX_RAILS_VERSION: "7.0" - name: Install gems From 5e5b6e500eca9505df5c2cac69400a4ce55aad6d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 14:19:45 -0500 Subject: [PATCH 375/524] Drop workaround --- .github/workflows/plugins.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index fa704e2ae..61217409f 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -116,12 +116,8 @@ jobs: - name: clone https://github.com/lekemula/solargraph-rspec/ run: | cd .. - # git clone https://github.com/lekemula/solargraph-rspec.git - - # pending https://github.com/lekemula/solargraph-rspec/pull/30 - git clone https://github.com/apiology/solargraph-rspec.git + git clone https://github.com/lekemula/solargraph-rspec.git cd solargraph-rspec - git checkout reset_closures - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From 1d7106e9eb1a62856a2cd654619633eb668dc7b6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 14:23:51 -0500 Subject: [PATCH 376/524] Use is_a? in a simple if() with a union to refine types --- lib/solargraph/parser/flow_sensitive_typing.rb | 6 ++++++ spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index b6fa07863..1737dff27 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -316,6 +316,12 @@ def process_isa(isa_node, true_presences, false_presences) if_true[pin] ||= [] if_true[pin] << { type: ComplexType.parse(isa_type_name) } process_facts(if_true, true_presences) + + # @type Hash{Pin::LocalVariable => Array ComplexType}>} + if_false = {} + if_false[pin] ||= [] + if_false[pin] << { not_type: ComplexType.parse(isa_type_name) } + process_facts(if_false, false_presences) end # @param nilp_node [Parser::AST::Node] diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 992f19999..30aabd32c 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -38,8 +38,6 @@ def verify_repro(repr) end end ), 'test.rb') - pending 'FlowSensitiveTyping improvements' - api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 10]) expect(clip.infer.to_s).to eq('Repro1') From 254714504622cff62c431dd8ea1bed2786cdc9be Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 14:45:22 -0500 Subject: [PATCH 377/524] Fix specs --- spec/parser/flow_sensitive_typing_spec.rb | 28 +++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index 30aabd32c..f35ee097d 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -195,6 +195,22 @@ class Repro < ReproBase; end 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] @@ -338,8 +354,6 @@ def verify_repro(repr) clip = api_map.clip_at('test.rb', [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - pending 'FlowSensitiveTyping improvements' - clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('nil') @@ -555,10 +569,8 @@ def verify_repro(repr, throw_the_dice) clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - pending('supporting else after && on varname') - clip = api_map.clip_at('test.rb', [8, 10]) - expect(clip.infer.rooted_tags).to eq('nil') + expect(clip.infer.rooted_tags).to eq('::Integer, nil') end it 'uses variable in a simple if() to refine types' do @@ -605,7 +617,7 @@ def verify_repro(repr = nil) expect(clip.infer.rooted_tags).to eq('10') clip = api_map.clip_at('test.rb', [7, 10]) - expect(clip.infer.rooted_tags).to eq('nil') + expect(clip.infer.rooted_tags).to eq('nil, false') end it 'uses .nil? in a return if() in an if to refine types using nil checks' do @@ -668,8 +680,6 @@ def bar(arr, baz: nil) clip = api_map.clip_at('test.rb', [6, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - pending('better scoping of return if in blocks') - clip = api_map.clip_at('test.rb', [9, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') @@ -700,8 +710,6 @@ def bar(baz: nil) clip = api_map.clip_at('test.rb', [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - pending('better scoping of return if in unless') - clip = api_map.clip_at('test.rb', [10, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end From c411211db398c49864a4b20a959d4aff74ce88fb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 15:06:26 -0500 Subject: [PATCH 378/524] Use same ruby version for all typechecking for consistency --- .github/workflows/plugins.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 5e8cbf607..f0301298e 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -54,7 +54,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -83,7 +83,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: From 9c8ed514c1e9de86c21de45fac0f46a51a71031e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 15:18:12 -0500 Subject: [PATCH 379/524] Rename rule to match future PR --- lib/solargraph/shell.rb | 5 ++++- lib/solargraph/type_checker.rb | 8 +++++--- lib/solargraph/type_checker/rules.rb | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 1a8514922..faa1b8d6b 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -175,7 +175,10 @@ def typecheck *files # @sg-ignore Unresolved call to options level = options[:level].to_sym rules = Solargraph::TypeChecker::Rules.new(level) - api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout, loose_unions: rules.loose_unions?) + api_map = + Solargraph::ApiMap.load_with_cache(directory, $stdout, + loose_unions: + rules.require_all_unique_types_match_expected_on_lhs?) probcount = 0 if files.empty? files = api_map.source_maps.map(&:filename) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index bec458e94..a6fa5a9c4 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -30,7 +30,7 @@ def initialize filename, api_map: nil, level: :normal, rules: Rules.new(level) # @todo Smarter directory resolution @rules = rules @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename), - loose_unions: rules.loose_unions?) + loose_unions: rules.require_all_unique_types_match_expected_on_lhs?) # @type [Array] @marked_ranges = [] @@ -65,7 +65,8 @@ class << self def load filename, level = :normal source = Solargraph::Source.load(filename) rules = Rules.new(level) - api_map = Solargraph::ApiMap.new(loose_unions: rules.loose_unions?) + api_map = Solargraph::ApiMap.new(loose_unions: + rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end @@ -77,7 +78,8 @@ def load filename, level = :normal def load_string code, filename = nil, level = :normal source = Solargraph::Source.load_string(code, filename) rules = Rules.new(level) - api_map = Solargraph::ApiMap.new(loose_unions: rules.loose_unions?) + api_map = Solargraph::ApiMap.new(loose_unions: + rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 4984b3efe..166568f5c 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -50,7 +50,7 @@ def must_tag_or_infer? rank > LEVELS[:typed] end - def loose_unions? + def require_all_unique_types_match_expected_on_lhs? rank < LEVELS[:alpha] end From 7ea7bfdbf9cb21bd1b652e8a4305c778c29034e2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 15:30:32 -0500 Subject: [PATCH 380/524] Fix polarity --- lib/solargraph/shell.rb | 2 +- lib/solargraph/type_checker.rb | 6 +++--- lib/solargraph/type_checker/rules.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index cd0ba1a9b..f68e4160a 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -178,7 +178,7 @@ def typecheck *files api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout, loose_unions: - rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_match_expected_on_lhs?) probcount = 0 if files.empty? files = api_map.source_maps.map(&:filename) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index a6fa5a9c4..f0338f13c 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -30,7 +30,7 @@ def initialize filename, api_map: nil, level: :normal, rules: Rules.new(level) # @todo Smarter directory resolution @rules = rules @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename), - loose_unions: rules.require_all_unique_types_match_expected_on_lhs?) + loose_unions: !rules.require_all_unique_types_match_expected_on_lhs?) # @type [Array] @marked_ranges = [] @@ -66,7 +66,7 @@ def load filename, level = :normal source = Solargraph::Source.load(filename) rules = Rules.new(level) api_map = Solargraph::ApiMap.new(loose_unions: - rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end @@ -79,7 +79,7 @@ def load_string code, filename = nil, level = :normal source = Solargraph::Source.load_string(code, filename) rules = Rules.new(level) api_map = Solargraph::ApiMap.new(loose_unions: - rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 166568f5c..a02f5c837 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -51,7 +51,7 @@ def must_tag_or_infer? end def require_all_unique_types_match_expected_on_lhs? - rank < LEVELS[:alpha] + rank >= LEVELS[:alpha] end def validate_tags? From 41adab490ba696f6c6bd1a84845d1558fdd3e4b5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 24 Nov 2025 05:51:28 -0500 Subject: [PATCH 381/524] Fix type issue combining directives I found this in another branch while improving typechecking. I'm not aware of any practical impact from the bug. --- 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 511c7deb7..afc5af31f 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -144,7 +144,7 @@ def choose_longer(other, attr) def combine_directives(other) return self.directives if other.directives.empty? return other.directives if directives.empty? - [directives + other.directives].uniq + (directives + other.directives).uniq end # @param other [self] From f9ec72481784cdc852d4e3040bc297a33e61408b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 24 Nov 2025 05:56:36 -0500 Subject: [PATCH 382/524] Pull in additional build fix --- .github/workflows/rspec.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ecc3d9771..b213c5ed0 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,6 +35,10 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 + # + # match version in Gemfile.lock and use same version below + bundler: 2.5.23 bundler-cache: false - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile @@ -46,7 +50,7 @@ jobs: run: echo "gem 'tsort'" >> .Gemfile - name: Install gems run: | - bundle install + bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version - name: Run tests run: bundle exec rake spec From 08626af3bdba3788ef122bcc0d2401dc6f8abe6d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 27 Nov 2025 07:16:16 -0500 Subject: [PATCH 383/524] Push logic up to BaseVariable for future expansion --- lib/solargraph/pin/base_variable.rb | 106 ++++++++++++++++++++++++- lib/solargraph/pin/common.rb | 1 - lib/solargraph/pin/local_variable.rb | 112 +-------------------------- 3 files changed, 105 insertions(+), 114 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 5d7f78620..1b334164c 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -11,18 +11,28 @@ class BaseVariable < Base attr_accessor :mass_assignment + # @return [Range, nil] + attr_reader :presence + # @param return_type [ComplexType, nil] # @param assignment [Parser::AST::Node, nil] First assignment # that was made to this variable # @param assignments [Array] Possible # assignments that may have been made to this variable # @param mass_assignment [Array(Parser::AST::Node, Integer), nil] - def initialize assignment: nil, assignments: [], mass_assignment: nil, return_type: nil, **splat + # @param presence [Range, nil] + # @param presence_certain [Boolean] + # @param mass_assignment [Array(Parser::AST::Node, Integer), nil] + def initialize assignment: nil, assignments: [], mass_assignment: nil, + presence: nil, presence_certain: false, return_type: nil, + **splat super(**splat) @assignments = (assignment.nil? ? [] : [assignment]) + assignments # @type [nil, ::Array(Parser::AST::Node, Integer)] @mass_assignment = mass_assignment @return_type = return_type + @presence = presence + @presence_certain = presence_certain end def reset_generated! @@ -36,6 +46,8 @@ def combine_with(other, attrs={}) assignments: new_assignments, mass_assignment: combine_mass_assignment(other), return_type: combine_return_type(other), + presence: combine_presence(other), + presence_certain: combine_presence_certain(other), }) super(other, new_attrs) end @@ -49,6 +61,16 @@ def combine_mass_assignment(other) mass_assignment || other.mass_assignment end + # If a certain pin is being combined with an uncertain pin, we + # end up with a certain result + # + # @param other [self] + # + # @return [Boolean] + def combine_presence_certain(other) + presence_certain? || other.presence_certain? + end + # @return [Parser::AST::Node, nil] def assignment @assignment ||= assignments.last @@ -61,6 +83,10 @@ def combine_assignments(other) (other.assignments + assignments).uniq end + def inner_desc + super + ", presence=#{presence.inspect}, assignments=#{assignments}" + end + def completion_item_kind Solargraph::LanguageServer::CompletionItemKinds::VARIABLE end @@ -109,7 +135,7 @@ def return_types_from_node(parent_node, api_map) end # @param api_map [ApiMap] - # @return [ComplexType] + # @return [ComplexType, ComplexType::UniqueType] def probe api_map assignment_types = assignments.flat_map { |node| return_types_from_node(node, api_map) } type_from_assignment = ComplexType.new(assignment_types.flat_map(&:items).uniq) unless assignment_types.empty? @@ -144,6 +170,65 @@ def type_desc "#{super} = #{assignment&.type.inspect}" end + # @param other_loc [Location] + def starts_at?(other_loc) + location&.filename == other_loc.filename && + presence && + presence.start == other_loc.range.start + end + + # Narrow the presence range to the intersection of both. + # + # @param other [self] + # + # @return [Range, nil] + def combine_presence(other) + return presence || other.presence if presence.nil? || other.presence.nil? + + Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min) + end + + # @param other [self] + # @return [Pin::Closure, nil] + def combine_closure(other) + return closure if self.closure == other.closure + + # choose first defined, as that establishes the scope of the variable + if closure.nil? || other.closure.nil? + Solargraph.assert_or_log(:varible_closure_missing) do + "One of the local variables being combined is missing a closure: " \ + "#{self.inspect} vs #{other.inspect}" + end + return closure || other.closure + end + + if closure.location.nil? || other.closure.location.nil? + return closure.location.nil? ? other.closure : closure + end + + # if filenames are different, this will just pick one + return closure if closure.location <= other.closure.location + + other.closure + end + + # @param other_closure [Pin::Closure] + # @param other_loc [Location] + def visible_at?(other_closure, other_loc) + location.filename == other_loc.filename && + (!presence || presence.include?(other_loc.range.start)) && + visible_in_closure?(other_closure) + end + + def presence_certain? + @presence_certain + end + + protected + + # @return [Range] + attr_writer :presence + private # See if this variable is visible within 'viewing_closure' @@ -151,8 +236,11 @@ def type_desc # @param viewing_closure [Pin::Closure] # @return [Boolean] def visible_in_closure? viewing_closure + return false if closure.nil? + # if we're declared at top level, we can't be seen from within # methods declared tere + return false if viewing_closure.is_a?(Pin::Method) && closure.context.tags == 'Class<>' return true if viewing_closure.binder.namespace == closure.binder.namespace @@ -170,6 +258,20 @@ def visible_in_closure? viewing_closure visible_in_closure?(parent_of_viewing_closure) end + # @param other [self] + # @return [ComplexType, nil] + def combine_return_type(other) + if presence_certain? && return_type&.defined? + # flow sensitive typing has already figured out this type + # has been downcast - use the type it figured out + return return_type + end + if other.presence_certain? && other.return_type&.defined? + return other.return_type + end + combine_types(other, :return_type) + end + # @param other [self] # @param attr [::Symbol] # diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 04a886aa0..b7eeba716 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -22,7 +22,6 @@ def closure=(value) reset_generated! end - # @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred # @return [Pin::Closure, nil] def closure Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index a4c190261..f0547703a 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -3,126 +3,16 @@ module Solargraph module Pin class LocalVariable < BaseVariable - # @return [Range, nil] - attr_reader :presence - - def presence_certain? - @presence_certain - end - - # @param presence [Range, nil] - # @param presence_certain [Boolean] - # @param splat [Hash] - def initialize presence: nil, presence_certain: false, **splat - super(**splat) - @presence = presence - @presence_certain = presence_certain - end - def combine_with(other, attrs={}) # keep this as a parameter return other.combine_with(self, attrs) if other.is_a?(Parameter) && !self.is_a?(Parameter) - new_assignments = combine_assignments(other) - new_attrs = attrs.merge({ - presence: combine_presence(other), - presence_certain: combine_presence_certain(other), - }) - - super(other, new_attrs) - end - - def inner_desc - super + ", presence=#{presence.inspect}, presence_certain=#{presence_certain?}" - end - - # @param other_loc [Location] - def starts_at?(other_loc) - location&.filename == other_loc.filename && - presence && - presence.start == other_loc.range.start - end - - # @param other [self] - # @return [Pin::Closure, nil] - def combine_closure(other) - return closure if self.closure == other.closure - - # choose first defined, as that establishes the scope of the variable - if closure.nil? || other.closure.nil? - Solargraph.assert_or_log(:varible_closure_missing) do - "One of the local variables being combined is missing a closure: " \ - "#{self.inspect} vs #{other.inspect}" - end - return closure || other.closure - end - - if closure.location.nil? || other.closure.location.nil? - return closure.location.nil? ? other.closure : closure - end - - # if filenames are different, this will just pick one - return closure if closure.location <= other.closure.location - - other.closure - end - - # @param other_closure [Pin::Closure] - # @param other_loc [Location] - def visible_at?(other_closure, other_loc) - location.filename == other_loc.filename && - (!presence || presence.include?(other_loc.range.start)) && - visible_in_closure?(other_closure) + super end def to_rbs (name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped') end - - # @param other [self] - # @return [ComplexType, nil] - def combine_return_type(other) - if presence_certain? && return_type&.defined? - # flow sensitive typing has already figured out this type - # has been downcast - use the type it figured out - return return_type - end - if other.presence_certain? && other.return_type&.defined? - return other.return_type - end - combine_types(other, :return_type) - end - - def probe api_map - if presence_certain? && return_type&.defined? - # flow sensitive typing has already probed this type - use - # the type it figured out - return return_type.qualify(api_map, *gates) - end - - super - end - - # Narrow the presence range to the intersection of both. - # - # @param other [self] - # - # @return [Range, nil] - def combine_presence(other) - return presence || other.presence if presence.nil? || other.presence.nil? - - Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min) - end - - # If a certain pin is being combined with an uncertain pin, we - # end up with a certain result - # - # @param other [self] - # - # @return [Boolean] - def combine_presence_certain(other) - presence_certain? || other.presence_certain? - end end end end From 405d9bef1d3c1071cfaf4cadb5ce8f054352a5f9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 17:59:43 -0500 Subject: [PATCH 384/524] Handle a self resolution case --- lib/solargraph/pin/parameter.rb | 4 +++- spec/type_checker/levels/strong_spec.rb | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 797f7b5da..cd0de8c57 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -172,7 +172,9 @@ def typify api_map # sniff based on param tags new_type = closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map) - adjust_type api_map, new_type + return adjust_type api_map, new_type.self_to_type(full_context) if new_type.defined? + + adjust_type api_map, super.self_to_type(full_context) end # @param atype [ComplexType] diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 970435dc3..c7e695517 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,23 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'understands self type when passed as parameter' do + checker = type_checker(%( + class Location + # @return [String] + attr_reader :filename + + # @param other [self] + def <=>(other) + return nil unless other.is_a?(Location) + + filename <=> other.filename + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end + it 'does not complain on array dereference' do checker = type_checker(%( # @param idx [Integer, nil] an index From 5a4aae2dc35ac3834b80dcf1449ad8ed3278511c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 19:07:09 -0500 Subject: [PATCH 385/524] Drop @sg-ignores --- lib/solargraph/pin/base_variable.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 1a3488346..dc4ecce2b 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -52,12 +52,9 @@ def combine_with(other, attrs={}) assignment: assert_same(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 intersection_return_type: combine_types(other, :intersection_return_type), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 exclude_return_type: combine_types(other, :exclude_return_type), }) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end From f74a49dd7b3553679596977aa5665af9bc375fa1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 14:09:10 -0500 Subject: [PATCH 386/524] Add missing spec after experiencing subtle merge error --- spec/type_checker/levels/strong_spec.rb | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 3b96683cf..133a540f7 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -722,5 +722,33 @@ def bar expect(checker.problems.map(&:message)).to eq(["Foo#bar return type could not be inferred", "Unresolved call to round"]) end + + it 'uses cast type instead of defined type' do + checker = type_checker(%( + # frozen_string_literal: true + + class Base; end + + class Subclass < Base + # @return [String] + attr_reader :bar + end + + class Foo + # @param bases [::Array] + # @return [void] + def baz(bases) + # @param sub [Subclass] + bases.each do |sub| + puts sub.bar + end + end + end + )) + + # expect 'sub' to be treated as 'Subclass' inside the block, and + # an error when trying to declare sub as Subclass + expect(checker.problems.map(&:message)).not_to include('Unresolved call to bar on Base') + end end end From 8701af7039361113b6f48ab0b9ffb1a86b1fa63c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 15:11:58 -0500 Subject: [PATCH 387/524] Handle an issue brought out by future merge --- lib/solargraph/pin/parameter.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index cd0de8c57..ae86e262e 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -38,6 +38,16 @@ def combine_with(other, attrs={}) super(other, new_attrs) end + def combine_return_type(other) + out = super + if out.undefined? + # allow our return_type method to provide a better type + # using :param tag + out = nil + end + out + end + def keyword? [:kwarg, :kwoptarg].include?(decl) end From 08024bde06ab1a48ac6a98ff33745d933459daab Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 15:58:47 -0500 Subject: [PATCH 388/524] Fixes based on future branch type checking --- lib/solargraph/complex_type/unique_type.rb | 7 +++++++ lib/solargraph/pin/parameter.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index dc386cd24..41138207c 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -385,6 +385,13 @@ def map &block [block.yield(self)] end + # @yieldparam t [self] + # @yieldreturn [self] + # @return [Enumerable] + def each &block + [self].each &block + end + # @return [Array] def to_a [self] diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ae86e262e..51286f855 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -40,7 +40,7 @@ def combine_with(other, attrs={}) def combine_return_type(other) out = super - if out.undefined? + if out&.undefined? # allow our return_type method to provide a better type # using :param tag out = nil From 6b9bce8e58ce3d17e9e1f23f90aa81914ec85d3f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 12:22:31 -0500 Subject: [PATCH 389/524] Annotation fixes --- lib/solargraph/api_map.rb | 8 ++-- lib/solargraph/doc_map.rb | 6 ++- lib/solargraph/pin_cache.rb | 68 +++++++++++++++--------------- lib/solargraph/rbs_map.rb | 2 +- lib/solargraph/rbs_map/core_map.rb | 2 +- lib/solargraph/shell.rb | 1 + lib/solargraph/workspace.rb | 4 +- lib/solargraph/yardoc.rb | 4 +- 8 files changed, 49 insertions(+), 46 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 695de0230..f99de7ffa 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -30,7 +30,7 @@ def initialize pins: [] index pins end - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # @return [void] def self.reset_core out: nil @@core_map = RbsMap::CoreMap.new @@ -136,7 +136,7 @@ def core_pins @@core_map.pins end - # @param name [String] + # @param name [String, nil] # @return [YARD::Tags::MacroDirective, nil] def named_macro name store.named_macros[name] @@ -186,7 +186,7 @@ def self.load directory api_map end - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [void] def cache_all_for_doc_map! out @doc_map.cache_doc_map_gems!(out) @@ -209,7 +209,7 @@ class << self # # # @param directory [String] - # @param out [IO] The output stream for messages + # @param out [IO, StringIO, nil] The output stream for messages # # @return [ApiMap] def self.load_with_cache directory, out = $stderr diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index cc3a4aed6..a82d1eebd 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -36,6 +36,7 @@ def requires end alias required requires + # @sg-ignore Translate to something flow sensitive typing understands # @return [Array] def uncached_gemspecs if @uncached_gemspecs.nil? @@ -66,7 +67,7 @@ def any_uncached? end # Cache all pins needed for the sources in this doc_map - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # @return [void] def cache_doc_map_gems! out unless uncached_gemspecs.empty? @@ -109,7 +110,7 @@ def dependencies out: $stderr # # @param gemspec [Gem::Specification] # @param rebuild [Boolean] whether to rebuild the pins even if they are cached - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # # @return [void] def cache gemspec, rebuild: false, out: nil @@ -235,6 +236,7 @@ def change_gemspec_version gemspec, version # @return [Array] def fetch_dependencies gemspec, out: nil # @param spec [Gem::Dependency] + # @param deps [Set] only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" dep = Gem.loaded_specs[spec.name] diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index fb3b8d7a4..97f5f5d6a 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -29,7 +29,7 @@ def cached? gemspec # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # @return [void] def cache_gem gemspec:, rebuild: false, out: nil rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) @@ -50,7 +50,7 @@ def cache_gem gemspec:, rebuild: false, out: nil end # @param gemspec [Gem::Specification, Bundler::LazySpecification] - # @param rbs_version_cache_key [String] + # @param rbs_version_cache_key [String, nil] def suppress_yard_cache? gemspec, rbs_version_cache_key if gemspec.name == 'parser' && rbs_version_cache_key != RbsMap::CACHE_KEY_UNRESOLVED # parser takes forever to build YARD pins, but has excellent RBS collection pins @@ -59,7 +59,7 @@ def suppress_yard_cache? gemspec, rbs_version_cache_key false end - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # # @return [void] def cache_all_stdlibs out: $stderr @@ -93,8 +93,8 @@ def lookup_rbs_version_cache_key gemspec rbs_map.cache_key end - # @param gemspec [Gem::Specification] - # @param rbs_version_cache_key [String] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param rbs_version_cache_key [String, nil] # @param yard_pins [Array] # @param rbs_collection_pins [Array] # @return [void] @@ -103,16 +103,16 @@ def cache_combined_pins gemspec, rbs_version_cache_key, yard_pins, rbs_collectio serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins) end - # @param gemspec [Gem::Specification] - # @return [Array] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Array, nil] def deserialize_combined_pin_cache gemspec rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec) load_combined_gem(gemspec, rbs_version_cache_key) end - # @param gemspec [Gem::Specification] - # @param out [IO, nil] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [StringIO, IO, nil] # @return [void] def uncache_gem gemspec, out: nil PinCache.uncache(yardoc_path(gemspec), out: out) @@ -172,7 +172,7 @@ def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key: # @param build_yard [Boolean] # @param build_rbs_collection [Boolean] # @param build_combined [Boolean] - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # # @return [void] def build_combine_and_cache gemspec, @@ -199,7 +199,7 @@ def build_combine_and_cache gemspec, # @param build_yard [Boolean] # @param build_rbs_collection [Boolean] # @param build_combined [Boolean] - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # # @return [void] def log_cache_info gemspec, @@ -223,7 +223,7 @@ def log_cache_info gemspec, end # @param gemspec [Gem::Specification, Bundler::LazySpecification] - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [Array] def cache_yard_pins gemspec, out gem_yardoc_path = yardoc_path(gemspec) @@ -239,8 +239,8 @@ def combined_pins_in_memory PinCache.all_combined_pins_in_memory[yard_plugins] ||= {} end - # @param gemspec [Gem::Specification] - # @param _out [IO, nil] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param _out [StringIO, IO, nil] # @return [Array] def cache_rbs_collection_pins gemspec, _out rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path) @@ -258,8 +258,8 @@ def cache_rbs_collection_pins gemspec, _out pins end - # @param gemspec [Gem::Specification] - # @return [Array] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @return [Array, nil] def deserialize_yard_pin_cache gemspec cached = load_yard_gem(gemspec) if cached @@ -271,7 +271,7 @@ def deserialize_yard_pin_cache gemspec end # @param gemspec [Gem::Specification, Bundler::LazySpecification] - # @param rbs_version_cache_key [String] + # @param rbs_version_cache_key [String, nil] # @return [Array] def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key cached = load_rbs_collection_pins(gemspec, rbs_version_cache_key) @@ -288,7 +288,7 @@ def yard_path_components yard_plugins.sort.uniq.join('-')] end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [String] def yardoc_path gemspec File.join(PinCache.base_dir, @@ -296,39 +296,39 @@ def yardoc_path gemspec "#{gemspec.name}-#{gemspec.version}.yardoc") end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [String] def yard_gem_path gemspec File.join(PinCache.work_dir, *yard_path_components, "#{gemspec.name}-#{gemspec.version}.ser") end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [Array, nil] def load_yard_gem gemspec PinCache.load(yard_gem_path(gemspec)) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param pins [Array] # @return [void] def serialize_yard_gem gemspec, pins PinCache.save(yard_gem_path(gemspec), pins) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [Boolean] def yard_gem? gemspec exist?(yard_gem_path(gemspec)) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param hash [String, nil] # @return [String] def rbs_collection_pins_path gemspec, hash rbs_collection_pins_path_prefix(gemspec) + "#{hash || 0}.ser" end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [String] def rbs_collection_pins_path_prefix gemspec File.join(PinCache.work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-") @@ -342,7 +342,7 @@ def load_rbs_collection_pins gemspec, hash PinCache.load(rbs_collection_pins_path(gemspec, hash)) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param hash [String, nil] # @param pins [Array] # @return [void] @@ -350,20 +350,20 @@ def serialize_rbs_collection_pins gemspec, hash, pins PinCache.save(rbs_collection_pins_path(gemspec, hash), pins) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param hash [String, nil] # @return [String] def combined_path gemspec, hash File.join(combined_path_prefix(gemspec) + "-#{hash || 0}.ser") end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @return [String] def combined_path_prefix gemspec File.join(PinCache.work_dir, 'combined', yard_plugins.sort.join('-'), "#{gemspec.name}-#{gemspec.version}") end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param hash [String, nil] # @param pins [Array] # @return [void] @@ -377,7 +377,7 @@ def combined_gem? gemspec, hash exist?(combined_path(gemspec, hash)) end - # @param gemspec [Gem::Specification] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] # @param hash [String, nil] # @return [Array, nil] def load_combined_gem gemspec, hash @@ -388,8 +388,8 @@ def load_combined_gem gemspec, hash loaded end - # @param gemspec [Gem::Specification] - # @param hash [String] + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param hash [String, nil] def rbs_collection_pins? gemspec, hash exist?(rbs_collection_pins_path(gemspec, hash)) end @@ -447,7 +447,7 @@ def uncache *path_segments, out: nil end end - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [void] def uncache_core out: nil uncache(core_path, out: out) @@ -455,7 +455,7 @@ def uncache_core out: nil ApiMap.reset_core(out: out) end - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [void] def uncache_stdlib out: nil uncache(stdlib_path, out: out) @@ -629,7 +629,7 @@ def core? File.file?(core_path) end - # @param out [IO, nil] + # @param out [StringIO, IO, nil] # @return [Array] def cache_core out: $stderr RbsMap::CoreMap.new.cache_core(out: out) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index f6309bb55..a5b998ff0 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -43,7 +43,7 @@ def initialize library, version = nil, rbs_collection_config_path: nil, rbs_coll CACHE_KEY_STDLIB = 'stdlib' CACHE_KEY_LOCAL = 'local' - # @param cache_key [String] + # @param cache_key [String, nil] # @return [String, nil] a description of the source of the RBS info def self.rbs_source_desc cache_key case cache_key diff --git a/lib/solargraph/rbs_map/core_map.rb b/lib/solargraph/rbs_map/core_map.rb index 275dbbcf4..8fe2afc83 100644 --- a/lib/solargraph/rbs_map/core_map.rb +++ b/lib/solargraph/rbs_map/core_map.rb @@ -22,7 +22,7 @@ def pins out: $stderr @pins = cache_core(out: out) end - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # @return [Array] def cache_core out: $stderr new_pins = [] diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 735e6c290..2a45a67ec 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -80,6 +80,7 @@ def config(directory = '.') conf['extensions'].push m end end + # @param file [File] File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| file.puts conf.to_yaml end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 2697e90e0..72a802623 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -70,7 +70,7 @@ def global_environ end # @param gemspec [Gem::Specification, Bundler::LazySpecification] - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # # @return [void] @@ -79,7 +79,7 @@ def cache_gem gemspec, out: nil, rebuild: false end # @param gemspec [Gem::Specification, Bundler::LazySpecification] - # @param out [IO, nil] output stream for logging + # @param out [StringIO, IO, nil] output stream for logging # # @return [void] def uncache_gem gemspec, out: nil diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 50f212f13..a7f5d2ab8 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -35,8 +35,8 @@ def build_docs gem_yardoc_path, yard_plugins, gemspec end # @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem - # @param gemspec [Gem::Specification] - # @param out [IO, nil] where to log messages + # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param out [StringIO, IO, nil] where to log messages # @return [Array] def build_pins gem_yardoc_path, gemspec, out: $stderr yardoc = load!(gem_yardoc_path) From fca10824817356a24245ae8112716b9c2e9ececb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 12:27:33 -0500 Subject: [PATCH 390/524] Remove @sg-ignore --- 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 a82d1eebd..3e9e64212 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -36,7 +36,6 @@ def requires end alias required requires - # @sg-ignore Translate to something flow sensitive typing understands # @return [Array] def uncached_gemspecs if @uncached_gemspecs.nil? From 76c6d7f6d9a1b6e585b8437e996c03d9380d45ba Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 15:45:23 -0500 Subject: [PATCH 391/524] Add fix that this PR revealed on future branch --- lib/solargraph/pin/callable.rb | 5 +++++ lib/solargraph/pin/parameter.rb | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 207c2619b..93a69687b 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -21,6 +21,11 @@ def initialize block: nil, return_type: nil, parameters: [], **splat @parameters = parameters end + def reset_generated! + parameters.each(&:reset_generated!) + super + end + # @return [String] def method_namespace closure.namespace diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 947513689..ae7cc5416 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -135,6 +135,11 @@ def full end end + def reset_generated! + @return_type = nil + super + end + # @return [ComplexType] def return_type if @return_type.nil? From 892630cf68a19058297f2d2e8ea1d9d66e7cb464 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 16:03:58 -0500 Subject: [PATCH 392/524] Add fix that this PR revealed on future branch --- lib/solargraph/pin/parameter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ae7cc5416..9d7ab1c89 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -136,7 +136,7 @@ def full end def reset_generated! - @return_type = nil + @return_type = nil if param_tag super end From f879eb2c8c15e4e29083fb21995bcb1cc9d0fbc2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 16:19:16 -0500 Subject: [PATCH 393/524] Add @sg-ignore --- lib/solargraph/yardoc.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 09bcd4586..0afdf1482 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -23,6 +23,7 @@ def cache(yard_plugins, gemspec) yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel + # @sg-ignore Unrecognized keyword argument chdir to Open3.capture2e stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } From 5600ee1ff7275cd0f624a904392d53bc40076248 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 16:30:42 -0500 Subject: [PATCH 394/524] Remove @sg-ignores --- lib/solargraph/shell.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 046a74296..3b1c4952d 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -241,24 +241,17 @@ def list puts "#{workspace.filenames.length} files total." end - # @sg-ignore Unresolved call to desc desc 'pin [PATH]', 'Describe a pin', hide: true - # @sg-ignore Unresolved call to option option :rbs, type: :boolean, desc: 'Output the pin as RBS', default: false - # @sg-ignore Unresolved call to option option :typify, type: :boolean, desc: 'Output the calculated return type of the pin from annotations', default: false - # @sg-ignore Unresolved call to option option :references, type: :boolean, desc: 'Show references', default: false - # @sg-ignore Unresolved call to option option :probe, type: :boolean, desc: 'Output the calculated return type of the pin from annotations and inference', default: false - # @sg-ignore Unresolved call to option option :stack, type: :boolean, desc: 'Show entire stack of a method pin by including definitions in superclasses', default: false # @param path [String] The path to the method pin, e.g. 'Class#method' or 'Class.method' # @return [void] def pin path api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) is_method = path.include?('#') || path.include?('.') - # @sg-ignore Unresolved call to options if is_method && options[:stack] scope, ns, meth = if path.include? '#' [:instance, *path.split('#', 2)] @@ -280,7 +273,6 @@ def pin path $stderr.puts "Pin not found for path '#{path}'" exit 1 when Pin::Namespace - # @sg-ignore Unresolved call to options if options[:references] superclass_tag = api_map.qualify_superclass(pin.return_type.tag) superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag @@ -289,12 +281,9 @@ def pin path end pins.each do |pin| - # @sg-ignore Unresolved call to options if options[:typify] || options[:probe] type = ComplexType::UNDEFINED - # @sg-ignore Unresolved call to options type = pin.typify(api_map) if options[:typify] - # @sg-ignore Unresolved call to options type = pin.probe(api_map) if options[:probe] && type.undefined? print_type(type) next @@ -338,7 +327,6 @@ def do_cache gemspec, api_map # @param type [ComplexType] # @return [void] def print_type(type) - # @sg-ignore Unresolved call to options if options[:rbs] puts type.to_rbs else @@ -349,7 +337,6 @@ def print_type(type) # @param pin [Solargraph::Pin::Base] # @return [void] def print_pin(pin) - # @sg-ignore Unresolved call to options if options[:rbs] puts pin.to_rbs else From 939786ed30bce6718895f5cc1b5cfcb395fb4ac7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 16:38:44 -0500 Subject: [PATCH 395/524] Remove @sg-ignores --- lib/solargraph/yardoc.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 0afdf1482..09bcd4586 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -23,7 +23,6 @@ def cache(yard_plugins, gemspec) yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel - # @sg-ignore Unrecognized keyword argument chdir to Open3.capture2e stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } From d87f222640e484df7c67092d804b3a7b96210697 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 16:47:40 -0500 Subject: [PATCH 396/524] Use consistent Ruby versions for typechecking --- .github/workflows/plugins.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b5984f3cb..d1d6b9be6 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -54,7 +54,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -83,7 +83,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: 3.4 bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: From aa4000614189c0c61c641de22e413632517431fe Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Dec 2025 20:09:38 -0500 Subject: [PATCH 397/524] Bring in fixes for strong typechecking issues --- lib/solargraph/pin/parameter.rb | 2 +- lib/solargraph/yard_map/mapper.rb | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 9d7ab1c89..7daa34d32 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -205,7 +205,7 @@ def param_tag # @return [ComplexType] def typify_block_param api_map block_pin = closure - if block_pin.is_a?(Pin::Block) && block_pin.receiver + if block_pin.is_a?(Pin::Block) && block_pin.receiver && index return block_pin.typify_parameters(api_map)[index] end ComplexType::UNDEFINED diff --git a/lib/solargraph/yard_map/mapper.rb b/lib/solargraph/yard_map/mapper.rb index 592b3805e..d9cf90a5f 100644 --- a/lib/solargraph/yard_map/mapper.rb +++ b/lib/solargraph/yard_map/mapper.rb @@ -39,10 +39,11 @@ def generate_pins code_object @namespace_pins[code_object.path] = nspin result.push nspin if code_object.is_a?(YARD::CodeObjects::ClassObject) and !code_object.superclass.nil? - # This method of superclass detection is a bit of a hack. If - # the superclass is a Proxy, it is assumed to be undefined in its - # yardoc and converted to a fully qualified namespace. - superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) + # This method of superclass detection is a bit of a + # hack. If the superclass is a Proxy that can't be + # resolved', it is assumed to be undefined in its yardoc + # and converted to a fully qualified namespace. + superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) && code_object.type == :proxy "::#{code_object.superclass}" else code_object.superclass.to_s From 6c08617106f235ae4bc957b105c9c358caeaac50 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 21:49:08 -0500 Subject: [PATCH 398/524] Fix merge --- lib/solargraph/diagnostics/rubocop_helpers.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index 5f738bad9..fc458956e 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -24,7 +24,6 @@ def require_rubocop(version = nil) specs = e.specs raise InvalidRubocopVersionError, "could not find '#{e.name}' (#{e.requirement}) - "\ - \ # @sg-ignore Unresolved call to version on String "did find: [#{specs.map { |s| s.version.version }.join(', ')}]" end require 'rubocop' From bef2fb77e79e2a587effaf4e69a22bb83b359c86 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 22:10:19 -0500 Subject: [PATCH 399/524] Typechecking fix --- lib/solargraph/range.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 25bd43387..25fc8b661 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -81,7 +81,7 @@ def self.from_to l1, c1, l2, c2 # Get a range from a node. # - # @param node [AST::Node] + # @param node [::Parser::AST::Node] # @return [Range, nil] def self.from_node node if node&.loc && node.loc.expression From 2cb58999c5d045996d86d379e700205801a074ce Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 22:19:07 -0500 Subject: [PATCH 400/524] Fix RuboCop issue --- .rubocop_todo.yml | 1 - spec/convention/activesupport_concern_spec.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 535712315..02a74780c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1156,7 +1156,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' diff --git a/spec/convention/activesupport_concern_spec.rb b/spec/convention/activesupport_concern_spec.rb index b58cd6584..a0eae979a 100644 --- a/spec/convention/activesupport_concern_spec.rb +++ b/spec/convention/activesupport_concern_spec.rb @@ -149,7 +149,7 @@ class Base RBS end - it { should_not be_empty } + it { is_expected.not_to be_empty } it "has one item" do expect(method_pins.size).to eq(1) From adc11c6c01c106820825ddbfb4fdc1620891ceef Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 22:41:13 -0500 Subject: [PATCH 401/524] Fix typechecking error --- lib/solargraph/shell.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 69a246175..471f59f6f 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -173,7 +173,6 @@ def reporters # @return [void] def typecheck *files directory = File.realpath(options[:directory]) - # @sg-ignore Unresolved call to options level = options[:level].to_sym rules = Solargraph::TypeChecker::Rules.new(level) api_map = @@ -189,7 +188,6 @@ def typecheck *files filecount = 0 time = Benchmark.measure { files.each do |file| - # @sg-ignore Unresolved call to options checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: options[:level].to_sym) problems = checker.problems next if problems.empty? From 95b76c3d87eb09961445be4a16f21e170bff4884 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 23:24:32 -0500 Subject: [PATCH 402/524] Fix merge --- spec/doc_map_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index abb7f1c26..4113529f5 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -75,7 +75,6 @@ def global(doc_map) Solargraph::Environ.new( requires: ['convention_gem1', 'convention_gem2'] ) ->>>>>>> origin/master end end From e935c972a316bde1c7ec71e66500d00228789535 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 23:26:47 -0500 Subject: [PATCH 403/524] Fix RuboCop --- spec/doc_map_spec.rb | 15 --------------- spec/yardoc_spec.rb | 3 --- 2 files changed, 18 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 4113529f5..6ab97ef97 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -33,21 +33,6 @@ end end - context 'when deserialization takes a while' do - let(:pre_cache) { false } - let(:requires) { ['backport'] } - - before do - # proxy this method to simulate a long-running deserialization - allow(Benchmark).to receive(:measure) do |&block| - block.call - 5.0 - end - end - - expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive - end - 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. diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb index 02444254a..6cd575de0 100644 --- a/spec/yardoc_spec.rb +++ b/spec/yardoc_spec.rb @@ -40,9 +40,6 @@ describe '#build_docs' do let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } let(:gemspec) { workspace.find_gem('rubocop') } - let(:api_map) { Solargraph::ApiMap.new } - let(:doc_map) { api_map.doc_map } - let(:gemspec) { Gem::Specification.find_by_path('rubocop') } let(:output) { '' } before do From f5c63aa01ac15b16dbd3107887e304a0b148dbe9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 23:36:14 -0500 Subject: [PATCH 404/524] Fix strong typechecking issues --- lib/solargraph/library.rb | 2 +- lib/solargraph/parser/parser_gem/class_methods.rb | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 95b4337f7..7de06bacd 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -510,7 +510,7 @@ def pin_cache workspace.pin_cache end - # @return [Hash{String => Set}] + # @return [Hash{String => Array}] def source_map_external_require_hash @source_map_external_require_hash ||= {} end diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index a512cdb05..2daf22fc7 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -23,9 +23,6 @@ def parse code, filename = nil, line = 0 buffer = ::Parser::Source::Buffer.new(filename, line) buffer.source = code parser.parse(buffer) - # @sg-ignore Unresolved type Parser::SyntaxError, - # Parser::UnknownEncodingInMagicComment for variable e - # https://github.com/castwide/solargraph/pull/1005 rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e raise Parser::SyntaxError, e.message end From 95d0280c1480db02d558681c019f165650a1ef0f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Dec 2025 23:52:34 -0500 Subject: [PATCH 405/524] Fix merge --- lib/solargraph/workspace/gemspecs.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 38b46da30..f832cf57f 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -67,13 +67,16 @@ def resolve_require require # @return [Array] def fetch_dependencies gemspec, out: $stderr # @param spec [Gem::Dependency] + # @param deps [Set] only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps| Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" dep = Gem.loaded_specs[spec.name] # @todo is next line necessary? + # @sg-ignore Unresolved call to requirement on Gem::Dependency dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) deps.merge fetch_dependencies(dep) if deps.add?(dep) rescue Gem::MissingSpecError + # @sg-ignore Unresolved call to requirement on Gem::Dependency Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for " \ "#{gemspec.name} not found in RubyGems." end.to_a From 5c42eb74977e63474025c18573960c528a8423a3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 08:37:40 -0500 Subject: [PATCH 406/524] Drop some sg-ignores --- lib/solargraph/parser/parser_gem/node_methods.rb | 1 - lib/solargraph/pin/method.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b77c4cd47..02f790c00 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,7 +119,6 @@ def convert_hash node result end - # @sg-ignore Wrong argument type for AST::Node.new: type expected AST::_ToSym, received :nil NIL_NODE = ::Parser::AST::Node.new(:nil) # @param node [Parser::AST::Node] diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 86bf1cd09..e6e0f6503 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -92,7 +92,6 @@ def combine_with(other, attrs = {}) end new_attrs = { visibility: combine_visibility(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 explicit: explicit? || other.explicit?, block: combine_blocks(other), node: choose_node(other, :node), From 0090a80f13098015ea8158ec3ec83e43bec565cf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 08:42:44 -0500 Subject: [PATCH 407/524] Mark spec as working --- spec/source/chain/call_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 355b9ee18..065a96582 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -600,8 +600,6 @@ def k end it 'correctly looks up civars' do - pending('better civar support') - source = Solargraph::Source.load_string(%( class Foo BAZ = /aaa/ From 0bfd89fea5ebd5eb29196fdee527ecbe4cf3ca35 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 08:47:08 -0500 Subject: [PATCH 408/524] Drop sg-ignores --- lib/solargraph/pin/base_variable.rb | 2 -- lib/solargraph/pin/local_variable.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 9514aee8d..1d5f85c0e 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -28,8 +28,6 @@ def combine_with(other, attrs={}) # tells you if the arg is optional or not. Prefer a # provided value if we have one here since we can't rely on # it from RBS so we can infer from it and typecheck on it. - # - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 assignment: choose(other, :assignment), mass_assignment: assert_same(other, :mass_assignment), return_type: combine_return_type(other), diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index d78329521..9673d425d 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -21,12 +21,10 @@ def initialize presence: nil, presence_certain: false, **splat def combine_with(other, attrs={}) new_attrs = { - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 presence_certain: assert_same(other, :presence_certain?), }.merge(attrs) new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence) - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 super(other, new_attrs) end From c59316ed38a58d4bdd4abb04dee663a4a2427f12 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 08:51:40 -0500 Subject: [PATCH 409/524] Add @sg-ignore --- lib/solargraph/yardoc.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 907afb2de..9d857a0a6 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -32,6 +32,7 @@ def cache(yard_plugins, gemspec) yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel + # @sg-ignore Unrecognized keyword argument chdir to Open3.capture2e stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } From fb4f6a9855b38aa7b656a61f1f9e1aa04ccd0e43 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 09:43:52 -0500 Subject: [PATCH 410/524] Fix merge --- .github/workflows/rspec.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 20e064585..4a3388ce4 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,16 +29,10 @@ jobs: rbs-version: '3.9.5' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' - # Missing require in 'rbs collection update' - hopefully - # fixed in next RBS release - - ruby-version: 'head' - rbs-version: '4.0.0.dev.4' - - ruby-version: 'head' - rbs-version: '3.9.4' - - ruby-version: 'head' - rbs-version: '3.6.1' - ruby-version: '4.0' rbs-version: '3.6.1' + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS release - ruby-version: '4.0' rbs-version: '4.0.0.dev.4' steps: From dadf38fed46e0cb4a3d516feb2d9250af2247eb0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 09:50:17 -0500 Subject: [PATCH 411/524] Adjust annotations --- lib/solargraph/pin/local_variable.rb | 6 +++--- lib/solargraph/pin/method.rb | 2 +- lib/solargraph/pin/parameter.rb | 2 +- lib/solargraph/rbs_map/conversions.rb | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 51e16a094..8d28be2f6 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -39,12 +39,12 @@ def combine_with(other, attrs={}) # @param other_closure [Pin::Closure] # @param other_loc [Location] - # @sg-ignore Need to add nil check here + # @todo Need to add nil check here def visible_at?(other_closure, other_loc) - # @sg-ignore Need to add nil check here + # @todo Need to add nil check here location.filename == other_loc.filename && presence&.include?(other_loc.range.start) && - # @sg-ignore Need to add nil check here + # @todo Need to add nil check here match_named_closure(other_closure, closure) end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index e6e0f6503..cb408411b 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -538,7 +538,7 @@ def see_reference api_map docstring.ref_tags.each do |ref| # @sg-ignore ref should actually be an intersection type next unless ref.tag_name == 'return' && ref.owner - # @sg-ignore ref should actually be an intersection type + # @todo ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map) return result unless result.nil? end diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 0e0b999cd..23ce865ad 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -258,7 +258,7 @@ def see_reference heredoc, api_map, skip = [] heredoc.ref_tags.each do |ref| # @sg-ignore ref should actually be an intersection type next unless ref.tag_name == 'param' && ref.owner - # @sg-ignore ref should actually be an intersection type + # @todo ref should actually be an intersection type result = resolve_reference(ref.owner.to_s, api_map, skip) return result unless result.nil? end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index c07159ef8..10263a953 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -794,6 +794,9 @@ def other_type_to_tag type # e.g., singleton(String) type_tag(type.name) else + # RBS doesn't provide a common base class for its type AST nodes' + # + # @sg-ignore Unresolved call to location on Object Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" 'undefined' end From 086044739b32529657317726ff93601820b82222 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 10:25:01 -0500 Subject: [PATCH 412/524] Fix typecheck errors --- lib/solargraph/api_map/store.rb | 3 +++ lib/solargraph/complex_type.rb | 1 + lib/solargraph/library.rb | 2 ++ lib/solargraph/parser/parser_gem/node_methods.rb | 1 - lib/solargraph/pin/method.rb | 1 - lib/solargraph/rbs_map/conversions.rb | 1 - lib/solargraph/shell.rb | 1 - lib/solargraph/source/chain/call.rb | 7 +++++++ lib/solargraph/type_checker.rb | 5 ++++- lib/solargraph/workspace/require_paths.rb | 1 + 10 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index bc829ba5a..619a2fc32 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -244,8 +244,11 @@ def get_ancestors(fqns) next if refs.nil? # @param ref [String] refs.map(&:type).map(&:to_s).each do |ref| + # @sg-ignore Flow-sensitive typing should be able to handle redefinition next if ref.nil? || ref.empty? || visited.include?(ref) + # @sg-ignore Flow-sensitive typing should be able to handle redefinition ancestors << ref + # @sg-ignore Flow-sensitive typing should be able to handle redefinition queue << ref end end diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 1b9f96dfa..a20652baf 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -21,6 +21,7 @@ def initialize types = [UniqueType::UNDEFINED] items.delete_if { |i| i.name == 'false' || i.name == 'true' } items.unshift(ComplexType::BOOLEAN) end + # @type [Array] items = [UniqueType::UNDEFINED] if items.any?(&:undefined?) @items = items end diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 5c7851201..b51e3cc2b 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -259,11 +259,13 @@ def references_from filename, line, column, strip: false, only: false referenced&.path == pin.path end if pin.path == 'Class#new' + # @todo Flow-sensitive typing should allow shadowing of Kernel#caller caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first if caller.defined? found.select! do |loc| clip = api_map.clip_at(loc.filename, loc.range.start) other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first + # @todo Flow-sensitive typing should allow shadowing of Kernel#caller caller == other end else diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b77c4cd47..02f790c00 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,7 +119,6 @@ def convert_hash node result end - # @sg-ignore Wrong argument type for AST::Node.new: type expected AST::_ToSym, received :nil NIL_NODE = ::Parser::AST::Node.new(:nil) # @param node [Parser::AST::Node] diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 13e9cb77a..302702ad8 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -94,7 +94,6 @@ def combine_with(other, attrs = {}) end new_attrs = { visibility: combine_visibility(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 explicit: explicit? || other.explicit?, block: combine_blocks(other), node: choose_node(other, :node), diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 54bca0f73..c26b9e264 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -352,7 +352,6 @@ def global_decl_to_pin decl # @param context [Context] # @param scope [Symbol] :instance or :class # @param name [String] The name of the method - # @sg-ignore # @return [Symbol] def calculate_method_visibility(decl, context, closure, scope, name) override_key = [closure.path, scope, name] diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 4e3300336..8c4b3a02c 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -279,7 +279,6 @@ def pin path $stderr.puts "Pin not found for path '#{path}'" exit 1 when Pin::Namespace - # @sg-ignore Unresolved call to options if options[:references] superclass_tag = api_map.qualify_superclass(pin.return_type.tag) superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index d48a93549..88bb3c1b0 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -71,6 +71,7 @@ def resolve api_map, name_pin, locals def inferred_pins pins, api_map, name_pin, locals result = pins.map do |p| next p unless p.is_a?(Pin::Method) + # @sg-ignore Flow-sensitive typing should handle is_a? and next overloads = p.signatures # next p if overloads.empty? type = ComplexType::UNDEFINED @@ -79,10 +80,15 @@ 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 + # @sg-ignore Flow-sensitive typing should handle is_a? and next with_block, without_block = overloads.partition(&:block?) + # @sg-ignore Flow-sensitive typing should handle is_a? and next + # @type Array sorted_overloads = with_block + without_block # @type [Pin::Signature, nil] new_signature_pin = nil + # @sg-ignore Flow-sensitive typing should handle is_a? and next + # @param ol [Pin::Signature] sorted_overloads.each do |ol| next unless ol.arity_matches?(arguments, with_block?) match = true @@ -141,6 +147,7 @@ def inferred_pins pins, api_map, name_pin, locals end break if type.defined? end + # @sg-ignore Flow-sensitive typing should handle is_a? and next p = p.with_single_signature(new_signature_pin) unless new_signature_pin.nil? next p.proxy(type) if type.defined? if !p.macros.empty? diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index d49de7384..cb645a831 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -463,7 +463,7 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi ptype = ptype.self_to_type(pin.context) unless ptype.undefined? argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) - # @sg-ignore Unresolved call to defined? + # @todo Unresolved call to defined? if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end @@ -741,10 +741,13 @@ def fake_args_for(pin) with_opts = false with_block = false pin.parameters.each do |pin| + # @sg-ignore Flow-sensitive typing should be able to handle redefinition if [:kwarg, :kwoptarg, :kwrestarg].include?(pin.decl) with_opts = true + # @sg-ignore Flow-sensitive typing should be able to handle redefinition elsif pin.decl == :block with_block = true + # @sg-ignore Flow-sensitive typing should be able to handle redefinition elsif pin.decl == :restarg args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true) else diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index c8eea161b..9c0424a88 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -83,6 +83,7 @@ def require_path_from_gemspec_file gemspec_file_path return [] if hash.empty? hash['paths'].map { |path| File.join(base, path) } rescue StandardError => e + # @sg-ignore Flow-sensitive typing should allow redefinition of 'e' Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end From 8d1a96ad1cd8f76f1f001a11864522054bbae565 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 10:29:00 -0500 Subject: [PATCH 413/524] Fix merge --- spec/type_checker/levels/strong_spec.rb | 37 ------------------------- 1 file changed, 37 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index c6c335294..95648e190 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -661,43 +661,6 @@ def run_command expect(checker.problems.map(&:message)).to be_empty end - context 'with class name available in more than one gate' do - let(:checker) do - type_checker(%( - module Foo - module Bar - class Symbol - end - end - end - - module Foo - module Baz - class Quux - # @return [void] - def foo - objects_by_class(Bar::Symbol) - end - - # @generic T - # @param klass [Class>] - # @return [Set>] - def objects_by_class klass - # @type [Set>] - s = Set.new - s - end - end - end - end - )) - end - - it 'resolves class name correctly in generic resolution' do - expect(checker.problems.map(&:message)).to be_empty - end - end - it 'handles "while foo" flow sensitive typing correctly' do checker = type_checker(%( # @param a [String, nil] From 88591aa2e5824bf67b49041ad95bc7425dc6cc69 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 10:41:53 -0500 Subject: [PATCH 414/524] Fix merge --- lib/solargraph/api_map.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index cc3031ea5..67e953d27 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -512,7 +512,8 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, fqns = rooted_type.namespace namespace_pin = store.get_path_pins(fqns).first methods = if namespace_pin.is_a?(Pin::Constant) - type = namespace_pin.infer(self) + type = namespace_pin.typify(self) + type = namespace_pin.probe(self) unless type.defined? if type.defined? namespace_pin = store.get_path_pins(type.namespace).first get_methods(type.namespace, scope: scope, visibility: visibility).select { |p| p.name == name } From 4663e7d796724f99bac82f9b18b6ba22d37e9455 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:00:44 -0500 Subject: [PATCH 415/524] Fix some @sg-ignores --- lib/solargraph/pin/parameter.rb | 1 + lib/solargraph/workspace/gemspecs.rb | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 018926563..08b371129 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -206,6 +206,7 @@ def param_tag def typify_block_param api_map block_pin = closure if block_pin.is_a?(Pin::Block) && block_pin.receiver && index + # @sg-ignore flow-sensivie typing should handle is_a? with && return block_pin.typify_parameters(api_map)[index] end ComplexType::UNDEFINED diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index c34120451..20fe350ae 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -116,6 +116,7 @@ def fetch_dependencies gemspec, out: $stderr # @param runtime_dep [Gem::Dependency] # @param deps [Hash{String => Gem::Specification}] gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| + # @sg-ignore Unresolved call to requirement on Gem::Dependency dep = find_gem(runtime_dep.name, runtime_dep.requirement) next unless dep @@ -170,7 +171,7 @@ def to_gem_specification specish end nil end - # yay! + # yay! when Bundler::LazySpecification # materializing didn't work. Let's look in the local @@ -180,8 +181,11 @@ def to_gem_specification specish when Bundler::StubSpecification # turns a Bundler::StubSpecification into a # Gem::StubSpecification into a Gem::Specification + # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' specish = specish.stub + # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' if specish.respond_to?(:spec) + # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' specish.spec else # turn the crank again @@ -190,8 +194,8 @@ def to_gem_specification specish else @@warned_on_gem_type ||= false unless @@warned_on_gem_type - logger.warn 'Unexpected type while resolving gem: ' \ - "#{specish.class}" + # @sg-ignore Unresolved call to class on Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification + logger.warn "Unexpected type while resolving gem: #{specish.class}" @@warned_on_gem_type = true end nil From a48c40a5b879c95548c8e7a59d91cdd51973e0bc Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:05:15 -0500 Subject: [PATCH 416/524] Fix merge --- spec/shell_spec.rb | 110 --------------------------------------------- 1 file changed, 110 deletions(-) diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index c28403c7b..12daabe99 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -297,114 +297,4 @@ def bundle_exec(*cmd) end end end - - # @type cmd [Array] - # @return [String] - def bundle_exec(*cmd) - # run the command in the temporary directory with bundle exec - Bundler.with_unbundled_env do - output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}") - expect(status.success?).to be(true), "Command failed: #{output}" - output - end - end - - describe 'pin' do - let(:api_map) { instance_double(Solargraph::ApiMap) } - let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } - - before do - allow(Solargraph::Pin::Method).to receive(:===).with(to_s_pin).and_return(true) - allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) - allow(api_map).to receive(:get_path_pins).with('String#to_s').and_return([to_s_pin]) - end - - context 'with no options' do - it 'prints a pin' do - allow(to_s_pin).to receive(:inspect).and_return('pin inspect result') - - out = capture_both { shell.pin('String#to_s') } - - expect(out).to eq("pin inspect result\n") - end - end - - context 'with --rbs option' do - it 'prints a pin with RBS type' do - allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') - - out = capture_both do - shell.options = { rbs: true } - shell.pin('String#to_s') - end - expect(out).to eq("pin RBS result\n") - end - end - - context 'with --stack option' do - it 'prints a pin using stack results' do - allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') - - allow(api_map).to receive(:get_method_stack).and_return([to_s_pin]) - capture_both do - shell.options = { stack: true } - shell.pin('String#to_s') - end - expect(api_map).to have_received(:get_method_stack).with('String', 'to_s', scope: :instance) - end - - it 'prints a static pin using stack results' do - # allow(to_s_pin).to receive(:to_rbs).and_return('pin RBS result') - string_new_pin = instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) - - allow(api_map).to receive(:get_method_stack).with('String', 'new', scope: :class).and_return([string_new_pin]) - allow(Solargraph::Pin::Method).to receive(:===).with(string_new_pin).and_return(true) - allow(api_map).to receive(:get_path_pins).with('String.new').and_return([string_new_pin]) - capture_both do - shell.options = { stack: true } - shell.pin('String.new') - end - expect(api_map).to have_received(:get_method_stack).with('String', 'new', scope: :class) - end - end - - context 'with --typify option' do - it 'prints a pin with typify type' do - allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) - - out = capture_both do - shell.options = { typify: true } - shell.pin('String#to_s') - end - expect(out).to eq("::String\n") - end - end - - context 'with --typify --rbs options' do - it 'prints a pin with typify type' do - allow(to_s_pin).to receive(:typify).and_return(Solargraph::ComplexType.parse('::String')) - - out = capture_both do - shell.options = { typify: true, rbs: true } - shell.pin('String#to_s') - end - expect(out).to eq("::String\n") - end - end - - context 'with no pin' do - it 'prints error' do - allow(api_map).to receive(:get_path_pins).with('Not#found').and_return([]) - allow(Solargraph::Pin::Method).to receive(:===).with(nil).and_return(false) - - out = capture_both do - shell.options = {} - shell.pin('Not#found') - rescue SystemExit - # Ignore the SystemExit raised by the shell when no pin is found - end - expect(out).to include("Pin not found for path 'Not#found'") - end - end - end end From 23e970e1918327b87515e310d96e46968fc0fb9d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:39:06 -0500 Subject: [PATCH 417/524] Merge branch 'intersection_types' into flow_sensitive_typing_2_0 --- .github/workflows/plugins.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index bb8c113b7..0a83f8aba 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -140,10 +140,10 @@ jobs: # this. (Gem::LoadError) bundle exec appraisal install bundle exec appraisal update date - # For some reason on ruby 3.1 it defaults to an old version: 1.3.2 - # https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22 - # We update manually to the latest - bundle exec appraisal update rspec-rails + # For some reason on ruby 3.1 it defaults to an old version: 1.3.2 + # https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22 + # We update manually to the latest + bundle exec appraisal update rspec-rails - name: Configure .solargraph.yml run: | cd ../solargraph-rspec From a2ce95f0970d61ef778ab4c550c2415b2a90cd20 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:41:14 -0500 Subject: [PATCH 418/524] Fix RuboCop issue --- spec/type_checker/levels/strong_spec.rb | 37 ------------------------- 1 file changed, 37 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index d475d6fc5..cd702d677 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -648,43 +648,6 @@ def baz expect(checker.problems.map(&:message)).to be_empty end - context 'with class name available in more than one gate' do - let(:checker) do - type_checker(%( - module Foo - module Bar - class Symbol - end - end - end - - module Foo - module Baz - class Quux - # @return [void] - def foo - objects_by_class(Bar::Symbol) - end - - # @generic T - # @param klass [Class>] - # @return [Set>] - def objects_by_class klass - # @type [Set>] - s = Set.new - s - end - end - end - end - )) - end - - it 'resolves class name correctly in generic resolution' do - expect(checker.problems.map(&:message)).to be_empty - end - end - it 'handles "while foo" flow sensitive typing correctly' do checker = type_checker(%( # @param a [String, nil] From 8b0cbf87ded35062a54893b3c978245d80be0e15 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:53:41 -0500 Subject: [PATCH 419/524] Fix type issues --- lib/solargraph/api_map.rb | 1 - lib/solargraph/api_map/store.rb | 1 - lib/solargraph/language_server/host.rb | 1 + lib/solargraph/language_server/host/sources.rb | 1 + lib/solargraph/location.rb | 1 - lib/solargraph/position.rb | 1 - lib/solargraph/range.rb | 4 ---- lib/solargraph/type_checker.rb | 5 ++--- lib/solargraph/yard_map/mapper/to_method.rb | 1 - lib/solargraph/yard_map/to_method.rb | 1 - 10 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index cc3031ea5..d5eaa2746 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -870,7 +870,6 @@ def resolve_method_alias(alias_pin) break if original end - # @sg-ignore ignore `received nil` for original create_resolved_alias_pin(alias_pin, original) if original end diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index bc829ba5a..34079fad7 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -71,7 +71,6 @@ def get_constants fqns, visibility = [:public] # @return [Enumerable] def get_methods fqns, scope: :instance, visibility: [:public] all_pins = namespace_children(fqns).select do |pin| - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 pin.is_a?(Pin::Method) && pin.scope == scope && visibility.include?(pin.visibility) end GemPins.combine_method_pins_by_path(all_pins) diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index b228bdba6..d43f58a80 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -301,6 +301,7 @@ def prepare directory, name = nil end # @return [String] + # @sg-ignore Need detailed hash types def command_path options['commandPath'] || 'solargraph' end diff --git a/lib/solargraph/language_server/host/sources.rb b/lib/solargraph/language_server/host/sources.rb index da0c63b93..cdbaa8feb 100644 --- a/lib/solargraph/language_server/host/sources.rb +++ b/lib/solargraph/language_server/host/sources.rb @@ -55,6 +55,7 @@ def update uri, updater # @raise [FileNotFoundError] if the URI does not match an open source. # # @param uri [String] + # @sg-ignore Flow-sensitive typing should understand raise # @return [Solargraph::Source] def find uri open_source_hash[uri] || raise(Solargraph::FileNotFoundError, "Host could not find #{uri}") diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index df92668bf..713b4fef1 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -71,7 +71,6 @@ def self.from_node(node) # @param other [BasicObject] def == other return false unless other.is_a?(Location) - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 filename == other.filename and range == other.range end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 74606f142..2faa0a99b 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -112,7 +112,6 @@ def self.normalize object def == other return false unless other.is_a?(Position) - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 line == other.line and character == other.character end end diff --git a/lib/solargraph/range.rb b/lib/solargraph/range.rb index 86452d646..7a9bc0e30 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -27,12 +27,9 @@ def initialize start, ending # @param other [BasicObject] def <=>(other) return nil unless other.is_a?(Range) - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 if start == other.start - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 ending <=> other.ending else - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 start <=> other.start end end @@ -101,7 +98,6 @@ def self.from_expr expr def == other return false unless other.is_a?(Range) - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 start == other.start && ending == other.ending end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 9c1a4bb0f..66fdf9c1f 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -192,7 +192,7 @@ def ignored_pins def variable_type_tag_problems result = [] all_variables.each do |pin| - # @sg-ignore Need to add nil check here + # @todo Need to add nil check here if pin.return_type.defined? declared = pin.typify(api_map) next if declared.duck_type? @@ -462,9 +462,8 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi ptype = data[:qualified] ptype = ptype.self_to_type(pin.context) unless ptype.undefined? - # @sg-ignore https://github.com/castwide/solargraph/pull/1127 argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) - # @sg-ignore Unresolved call to defined? + # @todo Unresolved call to defined? if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}") end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index 0838b9f4f..138c9ed44 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -85,7 +85,6 @@ def get_parameters code_object, location, comments, pin # HACK: Skip `nil` and `self` parameters that are sometimes emitted # for methods defined in C # See https://github.com/castwide/solargraph/issues/345 - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a| Solargraph::Pin::Parameter.new( location: location, diff --git a/lib/solargraph/yard_map/to_method.rb b/lib/solargraph/yard_map/to_method.rb index 010db89a5..7380ff305 100644 --- a/lib/solargraph/yard_map/to_method.rb +++ b/lib/solargraph/yard_map/to_method.rb @@ -15,7 +15,6 @@ def get_parameters code_object, location, comments # HACK: Skip `nil` and `self` parameters that are sometimes emitted # for methods defined in C # See https://github.com/castwide/solargraph/issues/345 - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a| Solargraph::Pin::Parameter.new( location: location, From 7cb629b46d697c223034df0ad001b0faf52fee31 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 11:55:07 -0500 Subject: [PATCH 420/524] Fix type issues --- .github/workflows/rspec.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 61b8a9cdf..6f9438c9a 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -40,13 +40,13 @@ jobs: - ruby-version: '3.4' # only include the 4.0 variants we include later - ruby-version: '4.0' - # Missing require in 'rbs collection update' - hopefully - # fixed in next RBS release - ruby-version: '4.0' rbs-version: '4.0.0.dev.4' - ruby-version: 'head' rbs-version: '3.9.4' - - ruby-version: 'head' + # Missing require in 'rbs collection update' - hopefully + # fixed in next RBS release + - ruby-version: '4.0' rbs-version: '3.6.1' include: - ruby-version: '3.1' From 63832df6eb3c7fab8531ffe49b5d30ccd1c8e3e8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 12:09:56 -0500 Subject: [PATCH 421/524] Fix rspec.yml --- .github/workflows/rspec.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 6f9438c9a..00d45b96d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -42,12 +42,12 @@ jobs: - ruby-version: '4.0' - ruby-version: '4.0' rbs-version: '4.0.0.dev.4' - - ruby-version: 'head' + - ruby-version: '4.0' rbs-version: '3.9.4' # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release - ruby-version: '4.0' - rbs-version: '3.6.1' + include: - ruby-version: '3.1' rbs-version: '3.6.1' @@ -58,7 +58,7 @@ jobs: - ruby-version: '3.4' rbs-version: '4.0.0.dev.4' - ruby-version: '4.0' - rbs-version: '3.6.1' + rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 704e97c027c8583aa5f6f0d7d5f0870ace8e5138 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 12:46:33 -0500 Subject: [PATCH 422/524] Fix rspec.yml --- .github/workflows/rspec.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 00d45b96d..acad1048c 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -47,7 +47,7 @@ jobs: # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release - ruby-version: '4.0' - + rbs-version: '4.0.0.dev.4' include: - ruby-version: '3.1' rbs-version: '3.6.1' @@ -57,8 +57,6 @@ jobs: rbs-version: '4.0.0.dev.4' - ruby-version: '3.4' rbs-version: '4.0.0.dev.4' - - ruby-version: '4.0' - rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 21f05ec1d1347f35af4283a1e95500ffa2d5d6a3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 12:55:49 -0500 Subject: [PATCH 423/524] Add @sg-ignores --- lib/solargraph/pin/closure.rb | 1 + lib/solargraph/pin/common.rb | 1 + lib/solargraph/source_map.rb | 1 + lib/solargraph/source_map/data.rb | 2 ++ lib/solargraph/type_checker.rb | 4 ++++ lib/solargraph/workspace.rb | 1 + lib/solargraph/workspace/config.rb | 1 + 7 files changed, 11 insertions(+) diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 347e0229e..911e42d24 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -44,6 +44,7 @@ def context end end + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def binder @binder || context end diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 062099ee4..a87c46b72 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -41,6 +41,7 @@ def namespace end # @return [ComplexType] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def binder @binder || context end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 15b747760..16628d917 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -187,6 +187,7 @@ def data end # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def convention_pins @convention_pins || [] end diff --git a/lib/solargraph/source_map/data.rb b/lib/solargraph/source_map/data.rb index 453520414..b42f9184b 100644 --- a/lib/solargraph/source_map/data.rb +++ b/lib/solargraph/source_map/data.rb @@ -9,12 +9,14 @@ def initialize source end # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def pins generate @pins || [] end # @return [Array] + # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def locals generate @locals || [] diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 66fdf9c1f..835997334 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -284,8 +284,11 @@ def call_problems end closest = found.typify(api_map) if found # @todo remove the internal_or_core? check at a higher-than-strict level + # @sg-ignore Change to something flow-sensitive typing understands if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) + # @sg-ignore Change to something flow-sensitive typing understands unless closest.generic? || ignored_pins.include?(found) + # @sg-ignore Change to something flow-sensitive typing understands if closest.defined? result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") else @@ -629,6 +632,7 @@ def declared_externally? pin base = base.base end closest = found.typify(api_map) if found + # @sg-ignore Change to something flow-sensitive typing understands if !found || closest.defined? || internal?(found) return false end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 655104307..e77861182 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -144,6 +144,7 @@ def synchronize! updater end # @return [String] + # @sg-ignore Need to validate config def command_path server['commandPath'] || 'solargraph' end diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index 3e30e5d74..181564c97 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -79,6 +79,7 @@ def required # An array of load paths for required paths. # # @return [Array] + # @sg-ignore Need to validate config def require_paths raw_data['require_paths'] || [] end From 103ed36ac13b084e176ed08fc8dc0a84d00aff26 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 13:41:39 -0500 Subject: [PATCH 424/524] Remove @sg-ignores --- lib/solargraph/pin/closure.rb | 1 - lib/solargraph/pin/local_variable.rb | 2 -- lib/solargraph/source/chain/call.rb | 3 --- lib/solargraph/type_checker.rb | 4 ---- 4 files changed, 10 deletions(-) diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index 911e42d24..347e0229e 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -44,7 +44,6 @@ def context end end - # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def binder @binder || context end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 7b59db1eb..efd94c5ff 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -55,9 +55,7 @@ def combine_closure(other) # @param other_closure [Pin::Closure] # @param other_loc [Location] - # @sg-ignore Need to add nil check here def visible_at?(other_closure, other_loc) - # @sg-ignore Need to add nil check here location.filename == other_loc.filename && (!presence || presence.include?(other_loc.range.start)) && visible_in_closure?(other_closure) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 42743d9f3..da72a557a 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -75,7 +75,6 @@ def resolve api_map, name_pin, locals def inferred_pins pins, api_map, name_pin, locals result = pins.map do |p| next p unless p.is_a?(Pin::Method) - # @sg-ignore Flow-sensitive typing should handle is_a? and next overloads = p.signatures # next p if overloads.empty? type = ComplexType::UNDEFINED @@ -84,7 +83,6 @@ 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 - # @sg-ignore Flow-sensitive typing should handle is_a? and next with_block, without_block = overloads.partition(&:block?) # @sg-ignore Flow-sensitive typing should handle is_a? and next # @type Array @@ -151,7 +149,6 @@ def inferred_pins pins, api_map, name_pin, locals end break if type.defined? end - # @sg-ignore Flow-sensitive typing should handle is_a? and next p = p.with_single_signature(new_signature_pin) unless new_signature_pin.nil? next p.proxy(type) if type.defined? if !p.macros.empty? diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 7ebae1f1a..a4c73ab8a 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -327,11 +327,8 @@ def call_problems all_closest = all_found.map { |pin| pin.typify(api_map) } closest = ComplexType.new(all_closest.flat_map(&:items).uniq) # @todo remove the internal_or_core? check at a higher-than-strict level - # @sg-ignore Change to something flow-sensitive typing understands if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) - # @sg-ignore Change to something flow-sensitive typing understands unless closest.generic? || ignored_pins.include?(found) - # @sg-ignore Change to something flow-sensitive typing understands if closest.defined? result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") else @@ -680,7 +677,6 @@ def declared_externally? pin end all_closest = all_found.map { |pin| pin.typify(api_map) } closest = ComplexType.new(all_closest.flat_map(&:items).uniq) - # @sg-ignore Change to something flow-sensitive typing understands if !found || closest.defined? || internal?(found) return false end From 2b753e87d0ded080af7fa5c4afa7b22f6ff84e87 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 16:47:28 -0500 Subject: [PATCH 425/524] Bump RBS versions in rspec test --- .github/workflows/rspec.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 73edd47a3..db84786dd 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -21,8 +21,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0'] - rbs-version: ['3.6.1', '3.9.5', '4.0.0.dev.4'] + ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'head'] + rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.4'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 @@ -42,16 +42,19 @@ jobs: # fixed in next RBS release - ruby-version: '4.0' rbs-version: '3.6.1' - - ruby-version: '4.0' + - ruby-version: 'head' + rbs-version: '3.6.1' include: - ruby-version: '3.1' rbs-version: '3.6.1' - ruby-version: '3.2' - rbs-version: '3.9.4' + rbs-version: '3.8.1' - ruby-version: '3.3' - rbs-version: '4.0.0.dev.4' + rbs-version: '3.8.5' - ruby-version: '3.4' - rbs-version: '4.0.0.dev.4' + rbs-version: '3.10.0' + - ruby-version: '3.4' + rbs-version: '4.0.0.dev' steps: - uses: actions/checkout@v3 - name: Set up Ruby From e182a53fe173d866e6d7ca4c256a5cdf7d774c55 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 16:48:26 -0500 Subject: [PATCH 426/524] Fix version --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index db84786dd..95d721900 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -54,7 +54,7 @@ jobs: - ruby-version: '3.4' rbs-version: '3.10.0' - ruby-version: '3.4' - rbs-version: '4.0.0.dev' + rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 34cdf784ad06ae9688c04a65af12310859ebca5a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 16:50:37 -0500 Subject: [PATCH 427/524] Fix version matrix --- .github/workflows/rspec.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 95d721900..406268714 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -25,9 +25,11 @@ jobs: rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.4'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: - # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 + # Ruby 3.0 doesn't work with RBS >=3.9 - ruby-version: '3.0' rbs-version: '3.9.5' + - ruby-version: '3.0' + rbs-version: '3.8.1' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' # only include the 3.1 variants we include later @@ -38,12 +40,10 @@ jobs: - ruby-version: '3.3' # only include the 3.4 variants we include later - ruby-version: '3.4' - # Missing require in 'rbs collection update' - hopefully - # fixed in next RBS release - - ruby-version: '4.0' - rbs-version: '3.6.1' - - ruby-version: 'head' - rbs-version: '3.6.1' + # - ruby-version: '4.0' + # rbs-version: '3.6.1' + # - ruby-version: 'head' + # rbs-version: '3.6.1' include: - ruby-version: '3.1' rbs-version: '3.6.1' From 851d142473fd362a04c0c3bca6aad97e3c06224e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 16:58:47 -0500 Subject: [PATCH 428/524] Fix version matrix --- .github/workflows/rspec.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 406268714..7913bdabf 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -25,13 +25,8 @@ jobs: rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.4'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: - # Ruby 3.0 doesn't work with RBS >=3.9 - - ruby-version: '3.0' - rbs-version: '3.9.5' - - ruby-version: '3.0' - rbs-version: '3.8.1' - - ruby-version: '3.0' - rbs-version: '4.0.0.dev.4' + # only include the 3.0 variants we include later + - ruby-version: '3.1' # only include the 3.1 variants we include later - ruby-version: '3.1' # only include the 3.2 variants we include later @@ -40,11 +35,18 @@ jobs: - ruby-version: '3.3' # only include the 3.4 variants we include later - ruby-version: '3.4' + # only include the 4.0 variants we include later + - ruby-version: '4.0' + # Don't exclude 'head' - let's test all RBS versions we + # can there + # - ruby-version: '4.0' # rbs-version: '3.6.1' # - ruby-version: 'head' # rbs-version: '3.6.1' include: + - ruby-version: '3.0' + rbs-version: '3.6.1' - ruby-version: '3.1' rbs-version: '3.6.1' - ruby-version: '3.2' From e2d27c9dcaec14b90cd8f3e262dc8ca36d3a28c3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 16:59:50 -0500 Subject: [PATCH 429/524] Fix version matrix --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 7913bdabf..94a714508 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -26,7 +26,7 @@ jobs: # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: # only include the 3.0 variants we include later - - ruby-version: '3.1' + - ruby-version: '3.0' # only include the 3.1 variants we include later - ruby-version: '3.1' # only include the 3.2 variants we include later From 8df808a8bf5cf3b13c3a46d41e4c1ff88286e122 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 17:01:05 -0500 Subject: [PATCH 430/524] Fix version matrix --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 94a714508..dcb726445 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -52,7 +52,7 @@ jobs: - ruby-version: '3.2' rbs-version: '3.8.1' - ruby-version: '3.3' - rbs-version: '3.8.5' + rbs-version: '3.9.5' - ruby-version: '3.4' rbs-version: '3.10.0' - ruby-version: '3.4' From 9974481a534d2e7c386121c01142c933f90c120b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 17:10:50 -0500 Subject: [PATCH 431/524] Fix version matrix --- .github/workflows/rspec.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index dcb726445..ced8df10d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -40,10 +40,12 @@ jobs: # Don't exclude 'head' - let's test all RBS versions we # can there - # - ruby-version: '4.0' - # rbs-version: '3.6.1' - # - ruby-version: 'head' - # rbs-version: '3.6.1' + # For some reason 'rbs collection install' is broken in + # 4.0.0.dev.4 on newer Rubies: + # + # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 + - ruby-version: 'head' + rbs-version: '4.0.0.dev.4' include: - ruby-version: '3.0' rbs-version: '3.6.1' From 56e25350a0bffe071172d3f563bf4935f6290e56 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 17:22:43 -0500 Subject: [PATCH 432/524] Exclude another --- .github/workflows/rspec.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ced8df10d..65eb8cac8 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -40,10 +40,9 @@ jobs: # Don't exclude 'head' - let's test all RBS versions we # can there - # For some reason 'rbs collection install' is broken in - # 4.0.0.dev.4 on newer Rubies: - # # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 + - ruby-version: 'head' + rbs-version: '3.8.1' - ruby-version: 'head' rbs-version: '4.0.0.dev.4' include: From d9e49366f2de9f8093c3bd48eecb566ddc751af2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 17:29:22 -0500 Subject: [PATCH 433/524] Exclude another --- .github/workflows/rspec.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 65eb8cac8..39475cc1d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -41,6 +41,8 @@ jobs: # can there # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 + - ruby-version: 'head' + rbs-version: '3.6.1' - ruby-version: 'head' rbs-version: '3.8.1' - ruby-version: 'head' From 751430271fbb5ac9b899923508200605171320b0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 17:52:29 -0500 Subject: [PATCH 434/524] Add version, fix doc --- .github/workflows/rspec.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 39475cc1d..f761b61aa 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -38,8 +38,12 @@ jobs: # only include the 4.0 variants we include later - ruby-version: '4.0' # Don't exclude 'head' - let's test all RBS versions we - # can there - + # can there. + # + # + # Just exclude some odd-ball compatibility issues we can't + # work around: + # # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 - ruby-version: 'head' rbs-version: '3.6.1' @@ -56,6 +60,8 @@ jobs: rbs-version: '3.8.1' - ruby-version: '3.3' rbs-version: '3.9.5' + - ruby-version: '3.3' + rbs-version: '3.10.0' - ruby-version: '3.4' rbs-version: '3.10.0' - ruby-version: '3.4' From 935bc9c4d67ef1567dbc85306dec2e7fc5d217fa Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 31 Dec 2025 18:42:24 -0500 Subject: [PATCH 435/524] init -> config --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index febc972ee..044018cc6 100755 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Plug-ins and extensions are available for the following editors: Solargraph's behavior can be controlled via optional [configuration](https://solargraph.org/guides/configuration) files. The highest priority file is a `.solargraph.yml` file at the root of the project. If not present, any global configuration at `~/.config/solargraph/config.yml` will apply. The path to the global configuration can be overridden with the `SOLARGRAPH_GLOBAL_CONFIG` environment variable. -Use `bundle exec solargraph init` to create a configuration file. +Use `bundle exec solargraph config` to create a configuration file. ### Plugins From 05265b9c0d801ac2af4929b61700cd7edf045e2d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 10:40:09 -0500 Subject: [PATCH 436/524] Fix rbs-version for Ruby 4.0 in CI workflow --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 4a3388ce4..c82ade49b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,10 +29,10 @@ jobs: rbs-version: '3.9.5' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' - - ruby-version: '4.0' - rbs-version: '3.6.1' # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release + - ruby-version: '4.0' + rbs-version: '3.6.1' - ruby-version: '4.0' rbs-version: '4.0.0.dev.4' steps: From 58737d715dd06e2931062b693fe0b270c066a774 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 10:40:47 -0500 Subject: [PATCH 437/524] Clean up ruby-version entries in rspec.yml Removed deprecated ruby-version entries for RBS. --- .github/workflows/rspec.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 20e064585..c82ade49b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -31,12 +31,6 @@ jobs: rbs-version: '4.0.0.dev.4' # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release - - ruby-version: 'head' - rbs-version: '4.0.0.dev.4' - - ruby-version: 'head' - rbs-version: '3.9.4' - - ruby-version: 'head' - rbs-version: '3.6.1' - ruby-version: '4.0' rbs-version: '3.6.1' - ruby-version: '4.0' From b5d11573355cb5ee3193d13b1bfa52039599b011 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 10:41:22 -0500 Subject: [PATCH 438/524] Fix RBS version for Ruby 4.0 in workflow --- .github/workflows/rspec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 4a3388ce4..c82ade49b 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -29,10 +29,10 @@ jobs: rbs-version: '3.9.5' - ruby-version: '3.0' rbs-version: '4.0.0.dev.4' - - ruby-version: '4.0' - rbs-version: '3.6.1' # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release + - ruby-version: '4.0' + rbs-version: '3.6.1' - ruby-version: '4.0' rbs-version: '4.0.0.dev.4' steps: From db36785e89de1d70715347b6a35b0ac39dfc479a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 14:26:48 -0500 Subject: [PATCH 439/524] Fix merge --- lib/solargraph/type_checker.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 75f72df05..63a0796fc 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -101,8 +101,7 @@ class << self # @return [self] def load filename, level = :normal source = Solargraph::Source.load(filename) - workspace = Workspace.new(File.dirname(filename)) - rules = workspace.rules(level) + rules = Rules.new(level, {}) api_map = Solargraph::ApiMap.new(loose_unions: !rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) @@ -116,8 +115,7 @@ def load filename, level = :normal # @return [self] def load_string code, filename = nil, level = :normal, api_map: nil source = Solargraph::Source.load_string(code, filename) - workspace = Workspace.new(File.dirname(filename)) - rules = workspace.rules(level) + rules = Rules.new(level, {}) api_map ||= Solargraph::ApiMap.new(loose_unions: !rules.require_all_unique_types_match_expected_on_lhs?) api_map.map(source) From 977b18355a293deb5677e73303cdb02e11530534 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 20:03:51 -0500 Subject: [PATCH 440/524] Improve signature combination Use our generated RBS signature from parameters as a key to combine method signatures from RBS/YARD pins. This is closer to what RBS does than the current technique of using the arity alone, and fixes a key degenerate case in Integer#+ revealed by updated definitions used by recently released RBS gems --- lib/solargraph/doc_map.rb | 2 +- lib/solargraph/parser/parser_gem/class_methods.rb | 1 + lib/solargraph/parser/parser_gem/node_methods.rb | 1 - lib/solargraph/pin/callable.rb | 7 ++++++- lib/solargraph/pin/method.rb | 14 +++++++------- spec/pin/method_spec.rb | 10 ++++++++++ 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index e45ff0b65..136c02078 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -346,7 +346,7 @@ def gemspec_or_preference gemspec end # @param gemspec [Gem::Specification] - # @param version [Gem::Version] + # @param version [Gem::Version, String] # @return [Gem::Specification] def change_gemspec_version gemspec, version Gem::Specification.find_by_name(gemspec.name, "= #{version}") diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 2daf22fc7..c8fc186c8 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -19,6 +19,7 @@ def parse_with_comments code, filename = nil # @param filename [String, nil] # @param line [Integer] # @return [Parser::AST::Node] + # @sg-ignore Need to handle type of 'raise' def parse code, filename = nil, line = 0 buffer = ::Parser::Source::Buffer.new(filename, line) buffer.source = code diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b77c4cd47..02f790c00 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,7 +119,6 @@ def convert_hash node result end - # @sg-ignore Wrong argument type for AST::Node.new: type expected AST::_ToSym, received :nil NIL_NODE = ::Parser::AST::Node.new(:nil) # @param node [Parser::AST::Node] diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index edbc3f941..bfce92c24 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -215,8 +215,13 @@ def mandatory_positional_param_count parameters.count(&:arg?) end + # @return [String] + def parameters_to_rbs + rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + end + def to_rbs - rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + '-> ' + return_type.to_rbs + parameters_to_rbs + '-> ' + return_type.to_rbs end def block? diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 86bf1cd09..30afb8f48 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -37,21 +37,21 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil # @param signature_pins [Array] # @return [Array] def combine_all_signature_pins(*signature_pins) - # @type [Hash{Array => Array}] - by_arity = {} + # @type [Hash{String => Array}] + by_rbs = {} signature_pins.each do |signature_pin| - by_arity[signature_pin.arity] ||= [] - by_arity[signature_pin.arity] << signature_pin + by_rbs[signature_pin.parameters_to_rbs] ||= [] + by_rbs[signature_pin.parameters_to_rbs] << signature_pin end - by_arity.transform_values! do |same_arity_pins| + by_rbs.transform_values! do |same_rbs_pins| # @param memo [Pin::Signature, nil] # @param signature [Pin::Signature] - same_arity_pins.reduce(nil) do |memo, signature| + same_rbs_pins.reduce(nil) do |memo, signature| next signature if memo.nil? memo.combine_with(signature) end end - by_arity.values.flatten + by_rbs.values.flatten end # @param other [Pin::Method] diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index 283ef6d51..c2ef40448 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -549,6 +549,16 @@ class Foo expect(pin.return_type).to be_undefined end + it 'combines signatures by type' do + # Integer+ in RBS is a number of signatures that dispatch based + # on type. Let's make sure we combine those with anything else + # found (e.g., additions from the BigDecimal RBS collection) + # without collapsing signatures + api_map = Solargraph::ApiMap.load_with_cache(Dir.pwd, nil) + method = api_map.get_method_stack('Integer', '+', scope: :instance).first + expect(method.signatures.count).to be > 3 + end + it 'infers untagged types from instance variables' do source = Solargraph::Source.load_string(%( class Foo From 81927c837dd9e5fbc367647b7afca61c22abe812 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 20:22:14 -0500 Subject: [PATCH 441/524] Drop annotation --- lib/solargraph/parser/parser_gem/class_methods.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index c8fc186c8..2daf22fc7 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -19,7 +19,6 @@ def parse_with_comments code, filename = nil # @param filename [String, nil] # @param line [Integer] # @return [Parser::AST::Node] - # @sg-ignore Need to handle type of 'raise' def parse code, filename = nil, line = 0 buffer = ::Parser::Source::Buffer.new(filename, line) buffer.source = code From 1b931447c424f8597491f7d3ccb76aa3e2944333 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 20:24:01 -0500 Subject: [PATCH 442/524] Fix RuboCop issue --- lib/solargraph/workspace.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 06980e6d0..156855a1b 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -70,10 +70,10 @@ def merge *sources includes_any = false sources.each do |source| - if directory == "*" || config.calculated.include?(source.filename) - source_hash[source.filename] = source - includes_any = true - end + next unless directory == "*" || config.calculated.include?(source.filename) + + source_hash[source.filename] = source + includes_any = true end includes_any From 1f0c11d7f40a0e00f66727bcd1d9887f9c5929d5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 21:34:25 -0500 Subject: [PATCH 443/524] Fix merge --- spec/doc_map_spec.rb | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 6ab97ef97..376e7d9bd 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -37,39 +37,23 @@ # Requiring 'set' is unnecessary because it's already included in core. It # might make sense to log redundant requires, but a warning is overkill. allow(Solargraph.logger).to receive(:warn).and_call_original - Solargraph::DocMap.new(['set'], []) + Solargraph::DocMap.new(['set'], [], workspace) expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) end it 'ignores nil requires' do - expect { Solargraph::DocMap.new([nil], []) }.not_to raise_error + expect { Solargraph::DocMap.new([nil], [], workspace) }.not_to raise_error end it 'ignores empty requires' do - expect { Solargraph::DocMap.new([''], []) }.not_to raise_error + expect { Solargraph::DocMap.new([''], [], workspace) }.not_to raise_error end it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], []) + doc_map = Solargraph::DocMap.new(['rspec'], [], workspace) expect(doc_map.dependencies.map(&:name)).to include('rspec-core') end - it 'includes convention requires from environ' do - dummy_convention = Class.new(Solargraph::Convention::Base) do - def global(doc_map) - Solargraph::Environ.new( - requires: ['convention_gem1', 'convention_gem2'] - ) - end - end - - it 'logs timing' do - # force lazy evaluation - _pins = doc_map.pins - expect(out.string).to include('Deserialized ').and include(' gem pins ').and include(' ms') - end - end - context 'with an uncached but valid gemspec' do let(:requires) { ['uncached_gem'] } let(:pre_cache) { false } From 25b114852089ce87515ef34a294790a284d5ef39 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 21:42:06 -0500 Subject: [PATCH 444/524] Don't use solargraph-rspec branch --- .github/workflows/plugins.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index c7ad72cb4..3f34cbae1 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -116,12 +116,9 @@ jobs: - name: clone https://github.com/lekemula/solargraph-rspec/ run: | cd .. - # git clone https://github.com/lekemula/solargraph-rspec.git + git clone https://github.com/lekemula/solargraph-rspec.git - # pending https://github.com/lekemula/solargraph-rspec/pull/30 - git clone https://github.com/apiology/solargraph-rspec.git cd solargraph-rspec - git checkout reset_closures - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From f44141cc5788e5fcd802fef526214704792d0f69 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 21:58:08 -0500 Subject: [PATCH 445/524] Fix merge --- spec/gem_pins_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index cafed4cf6..4d2bb4ff5 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -46,9 +46,4 @@ expect(pin.location.filename).to end_with('task.rb') end end - - it 'does not error out when handed incorrect gemspec' do - gemspec = instance_double(Gem::Specification, name: 'foo', version: '1.0', gem_dir: '/not-there') - expect { Solargraph::GemPins.build_yard_pins([], gemspec) }.not_to raise_error - end end From 979850540f21ca22abdd078d83dacf0b987271dd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 1 Jan 2026 23:35:16 -0500 Subject: [PATCH 446/524] Debug --- .github/workflows/rspec.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index f761b61aa..ae507e2a5 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -51,6 +51,7 @@ jobs: rbs-version: '3.8.1' - ruby-version: 'head' rbs-version: '4.0.0.dev.4' + - ruby-version: 'head' # TEMP include: - ruby-version: '3.0' rbs-version: '3.6.1' @@ -89,8 +90,6 @@ jobs: run: | bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version - bundle list - bundle exec solargraph pin 'Bundler::Dsl#source' - name: Update types run: | bundle exec rbs collection update From 5edcd2d3226ef84f718d8f68990fd488a44fe366 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 11:44:49 -0500 Subject: [PATCH 447/524] Add another use of stdlib dependencies in RBS --- .github/workflows/rspec.yml | 1 - lib/solargraph/doc_map.rb | 14 +- rbs/fills/rubygems/0/dependency.rbs | 193 ++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 rbs/fills/rubygems/0/dependency.rbs diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index ae507e2a5..72b4a08cc 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -51,7 +51,6 @@ jobs: rbs-version: '3.8.1' - ruby-version: 'head' rbs-version: '4.0.0.dev.4' - - ruby-version: 'head' # TEMP include: - ruby-version: '3.0' rbs-version: '3.6.1' diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 7dd7b9a3c..891bbabb3 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -240,11 +240,9 @@ def fetch_dependencies gemspec, out: nil Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" dep = Gem.loaded_specs[spec.name] # @todo is next line necessary? - # @sg-ignore Unresolved call to requirement on Gem::Dependency dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) deps.merge fetch_dependencies(dep) if deps.add?(dep) rescue Gem::MissingSpecError - # @sg-ignore Unresolved call to requirement on Gem::Dependency Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems." end.to_a end @@ -252,7 +250,17 @@ def fetch_dependencies gemspec, out: nil # @param gemspec [Gem::Specification] # @return [Array] def only_runtime_dependencies gemspec - gemspec.dependencies - gemspec.development_dependencies + gemspec_deps = gemspec.dependencies - gemspec.development_dependencies + stdlib_dep_names = workspace.stdlib_dependencies(gemspec.name) + stdlib_deps = workspace.stdlib_dependencies(gemspec.name).flat_map do |dep_name| + # already know about this dependency + next [] if gemspec_deps.any? { |dep| dep.name == dep_name } + + stdlib_specs = resolve_path_to_gemspecs(dep_name) || [] + + stdlib_specs.map { |spec| Gem::Dependency.new spec.name, "= #{spec.version}" } + end + gemspec_deps + stdlib_deps end diff --git a/rbs/fills/rubygems/0/dependency.rbs b/rbs/fills/rubygems/0/dependency.rbs new file mode 100644 index 000000000..db572b80d --- /dev/null +++ b/rbs/fills/rubygems/0/dependency.rbs @@ -0,0 +1,193 @@ +# +# The Dependency class holds a Gem name and a Gem::Requirement. +# +class Gem::Dependency + @name: untyped + + @requirement: untyped + + @type: untyped + + @prerelease: untyped + + @version_requirements: untyped + + @version_requirement: untyped + + # + # Valid dependency types. + # + TYPES: ::Array[:development | :runtime] + + # + # Dependency name or regular expression. + # + attr_accessor name: untyped + + # + # Allows you to force this dependency to be a prerelease. + # + attr_writer prerelease: untyped + + # + # Constructs a dependency with `name` and `requirements`. The last argument can + # optionally be the dependency type, which defaults to `:runtime`. + # + def initialize: (untyped name, *untyped requirements) -> void + + def hash: () -> untyped + + def inspect: () -> untyped + + # + # Does this dependency require a prerelease? + # + def prerelease?: () -> untyped + + # + # Is this dependency simply asking for the latest version of a gem? + # + def latest_version?: () -> untyped + + def pretty_print: (untyped q) -> untyped + + # + # What does this dependency require? + # + def requirement: () -> untyped + + # + # + def requirements_list: () -> untyped + + def to_s: () -> ::String + + # + # Dependency type. + # + def type: () -> untyped + + # + # + def runtime?: () -> untyped + + def ==: (untyped other) -> untyped + + # + # Dependencies are ordered by name. + # + def <=>: (untyped other) -> untyped + + # + # Uses this dependency as a pattern to compare to `other`. This dependency will + # match if the name matches the other's name, and other has only an equal + # version requirement that satisfies this dependency. + # + def =~: (untyped other) -> (nil | false | untyped) + + # + # + alias === =~ + + # + # Does this dependency match the specification described by `name` and `version` + # or match `spec`? + # + # NOTE: Unlike #matches_spec? this method does not return true when the version + # is a prerelease version unless this is a prerelease dependency. + # + def match?: (untyped obj, ?untyped? version, ?bool allow_prerelease) -> (false | true | untyped) + + # + # Does this dependency match `spec`? + # + # NOTE: This is not a convenience method. Unlike #match? this method returns + # true when `spec` is a prerelease version even if this dependency is not a + # prerelease dependency. + # + def matches_spec?: (untyped spec) -> (false | true | untyped) + + # + # Merges the requirements of `other` into this dependency + # + def merge: (untyped other) -> untyped + + # + # + def matching_specs: (?bool platform_only) -> untyped + + # + # True if the dependency will not always match the latest version. + # + def specific?: () -> untyped + + # + # + def to_specs: () -> untyped + + # + # + def to_spec: () -> untyped + + # + # + def identity: () -> (:complete | :abs_latest | :latest | :released) + + def encode_with: (untyped coder) -> untyped +end From 8320f5b5cfc28d6d60e472e90db56edf0ee4ff02 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 11:54:39 -0500 Subject: [PATCH 448/524] Mock additional call --- spec/doc_map_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 376e7d9bd..1e50301f4 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -64,7 +64,7 @@ uncached_gemspec = Gem::Specification.new('uncached_gem', '1.0.0') allow(workspace).to receive_messages(fresh_pincache: pincache) allow(Gem::Specification).to receive(:find_by_path).with('uncached_gem').and_return(uncached_gemspec) - allow(workspace).to receive(:global_environ).and_return(Solargraph::Environ.new) + allow(workspace).to receive_messages(stdlib_dependencies: [], global_environ: Solargraph::Environ.new) allow(pincache).to receive(:deserialize_combined_pin_cache).with(uncached_gemspec).and_return(nil) expect(doc_map.uncached_gemspecs).to eq([uncached_gemspec]) end From 6730e8f50792809893f29cbd5c321764db7c9a3a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 12:12:44 -0500 Subject: [PATCH 449/524] Fix annotations --- .../language_server/message/extended/check_gem_version.rb | 1 - lib/solargraph/parser/parser_gem/class_methods.rb | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) 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 ead1eeaf2..0447a7feb 100644 --- a/lib/solargraph/language_server/message/extended/check_gem_version.rb +++ b/lib/solargraph/language_server/message/extended/check_gem_version.rb @@ -83,7 +83,6 @@ def available @fetched = true begin @available ||= begin - # @sg-ignore Variable type could not be inferred for tuple # @type [Gem::Dependency, nil] tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first if tuple.nil? diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 2daf22fc7..b3caa900a 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -30,7 +30,9 @@ def parse code, filename = nil, line = 0 # @return [::Parser::Base] def parser @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| + # @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser parser.diagnostics.all_errors_are_fatal = true + # @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser parser.diagnostics.ignore_warnings = true end end From 9a88f53a33107d63da42145db4ef227ad5a9c3de Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 12:16:49 -0500 Subject: [PATCH 450/524] Update types in rspec undercover --- .github/workflows/rspec.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 72b4a08cc..d9473bd3e 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -90,8 +90,7 @@ jobs: bundle _2.5.23_ install bundle update rbs # use latest available for this Ruby version - name: Update types - run: | - bundle exec rbs collection update + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec undercover: @@ -106,9 +105,15 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' + # see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119 + # + # match version in Gemfile.lock and use same version below + bundler: 2.5.23 bundler-cache: false - name: Install gems run: bundle install + - name: Update types + run: bundle exec rbs collection update - name: Run tests run: bundle exec rake spec - name: Check PR coverage From 00c2947c9b137026dfb3c97e4b86866d1fc45106 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 14:25:55 -0500 Subject: [PATCH 451/524] Debug --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 3602da38d..5555617c5 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -122,7 +122,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4 + ruby-version: '3.1' bundler-cache: false - name: Install gems run: | From 5df17d5645e689a044458428f70ba9389c287a85 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 14:31:23 -0500 Subject: [PATCH 452/524] Debug --- .github/workflows/plugins.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 5555617c5..3f34cbae1 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -123,6 +123,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.1' + rubygems: latest bundler-cache: false - name: Install gems run: | From c22e3105184644b6066afe536502e0c6adca3509 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 16:43:17 -0500 Subject: [PATCH 453/524] Drop incorrect rbs collection use in spec --- .github/workflows/plugins.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 3f34cbae1..edaebe3eb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -150,10 +150,12 @@ jobs: run: | cd ../solargraph-rspec cp .solargraph.yml.example .solargraph.yml - - name: Solargraph generate RSpec gems YARD and RBS pins + - name: Solargraph generate RSpec gems YARD pins run: | cd ../solargraph-rspec - bundle exec appraisal rbs collection update + # solargraph-rspec's specs don't pass a workspace, so it + # doesn't know where to look for the RBS collection - let's + # not load one so that the solargraph gems command below works rspec_gems=$(bundle exec appraisal ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) bundle exec appraisal solargraph gems $rspec_gems - name: Run specs From 0d6b68b01c70fd72582c2bfdf4dfef44998fa501 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 17:23:43 -0500 Subject: [PATCH 454/524] Update rubocop todo --- .rubocop_todo.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3e8845df2..6406cc522 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -106,7 +106,6 @@ Layout/EndOfLine: Exclude: - 'Gemfile' - 'Rakefile' - - 'lib/solargraph/source/encoding_fixes.rb' - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). @@ -575,7 +574,6 @@ RSpec/BeforeAfterAll: - '**/spec/rails_helper.rb' - '**/spec/support/**/*.rb' - 'spec/api_map_spec.rb' - - 'spec/doc_map_spec.rb' - 'spec/language_server/host/dispatch_spec.rb' - 'spec/language_server/protocol_spec.rb' @@ -1174,7 +1172,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' From d5d619e3445a166462de76a4f564877bec1556c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 2 Jan 2026 19:42:53 -0500 Subject: [PATCH 455/524] Revert change --- lib/solargraph/yard_map/mapper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/yard_map/mapper.rb b/lib/solargraph/yard_map/mapper.rb index d9cf90a5f..74584d603 100644 --- a/lib/solargraph/yard_map/mapper.rb +++ b/lib/solargraph/yard_map/mapper.rb @@ -43,7 +43,8 @@ def generate_pins code_object # hack. If the superclass is a Proxy that can't be # resolved', it is assumed to be undefined in its yardoc # and converted to a fully qualified namespace. - superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) && code_object.type == :proxy + # + superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) "::#{code_object.superclass}" else code_object.superclass.to_s From 40a915f62c0d2a0c885ace0981ab02003bdb789e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 Jan 2026 10:36:09 -0500 Subject: [PATCH 456/524] Use "type arity" to guide signature combination --- lib/solargraph/pin/callable.rb | 18 +++++- lib/solargraph/pin/method.rb | 99 ++++++++++++++++++++++----------- lib/solargraph/pin/parameter.rb | 5 ++ 3 files changed, 87 insertions(+), 35 deletions(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index bfce92c24..ed1c2d7c9 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -89,11 +89,25 @@ def blockless_parameters end end - # @return [Array] + # e.g., [["T"], "", "?", "foo:"] - parameter arity declarations, + # ignoring positional names. Used to match signatures. + # + # @return [Array, String>] def arity [generics, blockless_parameters.map(&:arity_decl), block&.arity] end + # e.g., [["T"], "1", "?3", "foo:5"] - parameter arity + # declarations, including the number of unique types in each + # parameter. Used to determine whether combining two + # signatures has lost useful information mapping specific + # parameter types to specific return types. + # + # @return [Array, String>] + def type_arity + [generics, blockless_parameters.map(&:type_arity_decl), block&.arity] + end + # @param generics_to_resolve [Enumerable] # @param arg_types [Array, nil] # @param return_type_context [ComplexType, nil] @@ -101,6 +115,7 @@ def arity # @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, @@ -150,6 +165,7 @@ def method_name # @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, diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 30afb8f48..a60b455d3 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -34,26 +34,6 @@ def initialize visibility: :public, explicit: true, block: :undefined, node: nil @anon_splat = anon_splat end - # @param signature_pins [Array] - # @return [Array] - def combine_all_signature_pins(*signature_pins) - # @type [Hash{String => Array}] - by_rbs = {} - signature_pins.each do |signature_pin| - by_rbs[signature_pin.parameters_to_rbs] ||= [] - by_rbs[signature_pin.parameters_to_rbs] << signature_pin - end - by_rbs.transform_values! do |same_rbs_pins| - # @param memo [Pin::Signature, nil] - # @param signature [Pin::Signature] - same_rbs_pins.reduce(nil) do |memo, signature| - next signature if memo.nil? - memo.combine_with(signature) - end - end - by_rbs.values.flatten - end - # @param other [Pin::Method] # @return [::Symbol] def combine_visibility(other) @@ -66,20 +46,6 @@ 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? } - 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 = {}) priority_choice = choose_priority(other) return priority_choice unless priority_choice.nil? @@ -485,6 +451,71 @@ def dodgy_visibility_source? private + # @param other [Pin::Method] + # @return [Array] + def combine_signatures(other) + all_undefined = signatures.all? { |sig| !sig.return_type&.defined? } + other_all_undefined = other.signatures.all? { |sig| !sig.return_type&.defined? } + if all_undefined && !other_all_undefined + other.signatures + elsif other_all_undefined && !all_undefined + signatures + else + combine_signatures_by_type_arity(*signatures, *other.signatures) + end + end + + # @param signature_pins [Array] + # + # @return [Array] + def combine_signatures_by_type_arity(*signature_pins) + # @type [Hash{Array => Array}] + by_type_arity = {} + signature_pins.each do |signature_pin| + by_type_arity[signature_pin.type_arity] ||= [] + by_type_arity[signature_pin.type_arity] << signature_pin + end + + by_type_arity.transform_values! do |same_type_arity_signatures| + combine_same_type_arity_signatures same_type_arity_signatures + end + by_type_arity.values.flatten + end + + # @param same_type_arity_signatures [Array] + # + # @return [Array] + def combine_same_type_arity_signatures(same_type_arity_signatures) + # This is an O(n^2) operation, so bail out if n is not small + return same_type_arity_signatures if same_type_arity_signatures.length > 10 + + # @param old_signatures [Array] + # @param new_signature [Pin::Signature] + same_type_arity_signatures.reduce([]) do |old_signatures, new_signature| + next [new_signature] if old_signatures.empty? + + found_merge = false + old_signatures.flat_map do |old_signature| + potential_new_signature = old_signature.combine_with(new_signature) + + if potential_new_signature.type_arity == old_signature.type_arity + # the number of types in each parameter and return type + # match, so we found compatible signatures to merge. If + # we increased the number of types, we'd potentially + # have taken away the ability to use parameter types to + # choose the correct return type (while Ruby doesn't + # dispatch based on type, RBS does distinguish overloads + # based on types, not just arity, allowing for type + # information describing how methods behave based on + # their input types) + old_signatures - [old_signature] + [potential_new_signature] + else + old_signatures + [new_signature] + end + end + end + end + # @param name [String] # @param asgn [Boolean] # diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 91c205921..ebcf7727f 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -72,6 +72,11 @@ def arity_decl end end + # @return [String] + def type_arity_decl + arity_decl + return_type.items.count.to_s + end + def arg? decl == :arg end From 11bf98728e3e4a6e8456521ef074cd47a8ee7ae2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 Jan 2026 10:41:42 -0500 Subject: [PATCH 457/524] Update rubocop todo --- .rubocop_todo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 962ac9bb6..f811c2102 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -468,6 +468,7 @@ Metrics/ClassLength: Exclude: - 'lib/solargraph/api_map.rb' - 'lib/solargraph/language_server/host.rb' + - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/rbs_map/conversions.rb' - 'lib/solargraph/type_checker.rb' @@ -1175,7 +1176,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' From 72131e89c0cdccdb2470107f6fa0c42037294f75 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 Jan 2026 11:02:40 -0500 Subject: [PATCH 458/524] Include return type arity in comparison --- lib/solargraph/pin/callable.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index ed1c2d7c9..de5869073 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -103,9 +103,16 @@ def arity # signatures has lost useful information mapping specific # parameter types to specific return types. # - # @return [Array, String>] + # @return [Array] def type_arity - [generics, blockless_parameters.map(&:type_arity_decl), block&.arity] + [generics, blockless_parameters.map(&:type_arity_decl), block&.type_arity] + end + + # Same as type_arity, but includes return type arity at the front. + # + # @return [Array] + def full_type_arity + [return_type&.items.count.to_s] + type_arity end # @param generics_to_resolve [Enumerable] From 334f4e06f86a814a5587a2abf62d54bc678dbb79 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 Jan 2026 11:46:36 -0500 Subject: [PATCH 459/524] Add dodgy return type --- lib/solargraph/pin/base.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 57d083453..3f9d95331 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -205,7 +205,9 @@ def combine_return_type(other) def dodgy_return_type_source? # uses a lot of 'Object' instead of 'self' - location&.filename&.include?('core_ext/object/') + location&.filename&.include?('core_ext/object/') || + # ditto + location&.filename&.include?('stdlib/date/0/date.rbs') end # when choices are arbitrary, make sure the choice is consistent From a2e59646a079dcafb6d54f493b61ccdfa232cc50 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 3 Jan 2026 17:00:35 -0500 Subject: [PATCH 460/524] Fix RuboCop issue --- lib/solargraph/pin/callable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index de5869073..8f247205c 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -112,7 +112,7 @@ def type_arity # # @return [Array] def full_type_arity - [return_type&.items.count.to_s] + type_arity + [return_type ? return_type.items.count.to_s : nil] + type_arity end # @param generics_to_resolve [Enumerable] From 0ac3cb444c962beea001daca71f64ab07650a681 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 12:08:44 -0500 Subject: [PATCH 461/524] Add Ruby 4.0 jobs --- .github/workflows/rspec.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index f761b61aa..b9848b7ab 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,8 +35,6 @@ jobs: - ruby-version: '3.3' # only include the 3.4 variants we include later - ruby-version: '3.4' - # only include the 4.0 variants we include later - - ruby-version: '4.0' # Don't exclude 'head' - let's test all RBS versions we # can there. # @@ -64,8 +62,6 @@ jobs: rbs-version: '3.10.0' - ruby-version: '3.4' rbs-version: '3.10.0' - - ruby-version: '3.4' - rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 3e3e4d9ea08b46da2c050c43376384ff52777dd0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 12:56:49 -0500 Subject: [PATCH 462/524] Exclude another combo --- .github/workflows/rspec.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index b9848b7ab..a5af74830 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -43,6 +43,8 @@ jobs: # work around: # # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 + - ruby-version: '4.0' + rbs-version: '4.0.0.dev.4' - ruby-version: 'head' rbs-version: '3.6.1' - ruby-version: 'head' From c7eefc29b1a4768049ea59c521c147a7288ec42f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 13:06:09 -0500 Subject: [PATCH 463/524] Exclude another combo --- .github/workflows/rspec.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index a5af74830..25ab551b2 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -35,6 +35,8 @@ jobs: - ruby-version: '3.3' # only include the 3.4 variants we include later - ruby-version: '3.4' + # only include the 4.0 variants we include later + - ruby-version: '4.0' # Don't exclude 'head' - let's test all RBS versions we # can there. # @@ -43,8 +45,6 @@ jobs: # work around: # # https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102 - - ruby-version: '4.0' - rbs-version: '4.0.0.dev.4' - ruby-version: 'head' rbs-version: '3.6.1' - ruby-version: 'head' @@ -63,6 +63,8 @@ jobs: - ruby-version: '3.3' rbs-version: '3.10.0' - ruby-version: '3.4' + rbs-version: '4.0.0.dev.4' + - ruby-version: '4.0' rbs-version: '3.10.0' steps: - uses: actions/checkout@v3 From 6e6e85d9bec7ab7730aca9d477bd740a732dece3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 15:12:59 -0500 Subject: [PATCH 464/524] Update rules to use report? --- lib/solargraph/type_checker/rules.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index 0f136301e..dd341caf2 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -56,10 +56,6 @@ def must_tag_or_infer? report?(:must_tag_or_infer, :strict) end - def require_all_unique_types_match_expected_on_lhs? - rank >= LEVELS[:alpha] - end - def validate_tags? report?(:validate_tags, :typed) end @@ -72,6 +68,10 @@ def require_all_unique_types_match_declared? report?(:require_all_unique_types_match_declared, :alpha) end + def require_all_unique_types_match_expected_on_lhs? + report?(:require_all_unique_types_match_expected_on_lhs, :alpha) + end + def require_no_undefined_args? report?(:require_no_undefined_args, :alpha) end From 79a2b2a7d8ba58d307854d85a96b1f2a45be9444 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 15:18:18 -0500 Subject: [PATCH 465/524] Fix merge --- spec/type_checker/levels/strong_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 41344b8ac..b333529d2 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -168,6 +168,21 @@ def bar expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') end + it 'calls out missing args after a defaulted param' do + checker = type_checker(%( + # @param a [String] + # @param b [String] + # @return [void] + def foo(a = 'foo', b); end + + # @return [void] + def bar + foo(123) + end + )) + expect(checker.problems.map(&:message)).to include('Not enough arguments to #foo') + end + it 'reports missing param tags' do checker = type_checker(%( class Foo From 60df579ceb8a12c141fd957d69e8b3380155f8db Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 15:21:17 -0500 Subject: [PATCH 466/524] Fix merge --- lib/solargraph/type_checker.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 63a0796fc..167ee500d 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -20,12 +20,10 @@ class TypeChecker # @param filename [String, nil] # @param api_map [ApiMap, nil] - # @param rules [Rules] Type checker rules object # @param level [Symbol] Don't complain about anything above this level # @param workspace [Workspace, nil] Workspace to use for loading # type checker rules modified by user config - # @param type_checker_rules [Hash{Symbol => Symbol}] Overrides for - # type checker rules - e.g., :report_undefined => :strong + # @param rules [Rules] Type checker rules object def initialize filename, api_map: nil, level: :normal, From caa81d466d8f9414951ce13bdb29d9687e5c913f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 4 Jan 2026 19:09:23 -0500 Subject: [PATCH 467/524] Drop dead code --- lib/solargraph/api_map.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 04cfed55d..6c145e618 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -23,9 +23,6 @@ class ApiMap # @return [Array] attr_reader :missing_docs - # @return [Solargraph::Workspace::Gemspecs] - attr_reader :gemspecs - # @param pins [Array] def initialize pins: [] @source_map_hash = {} From 4c6a84e2b532c0ae5e3b3805849aeaeae08f223f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 19:01:33 -0500 Subject: [PATCH 468/524] Bump version to 0.59.0.dev.1 --- 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 5bb8e52f8..b98600a95 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = '0.58.1' + VERSION = '0.59.0.dev.1' end From 494b2957887751fbaff156a608e5367b1f5c3693 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 19:07:58 -0500 Subject: [PATCH 469/524] Rename rule --- lib/solargraph/shell.rb | 2 +- lib/solargraph/type_checker.rb | 6 +++--- lib/solargraph/type_checker/rules.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 7c7bcc248..de5567394 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -179,7 +179,7 @@ def typecheck *files api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout, loose_unions: - !rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_support_call?) probcount = 0 if files.empty? files = api_map.source_maps.map(&:filename) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 167ee500d..2ff6140a5 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -32,7 +32,7 @@ def initialize filename, @filename = filename # @todo Smarter directory resolution @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename), - loose_unions: !rules.require_all_unique_types_match_expected_on_lhs?) + loose_unions: !rules.require_all_unique_types_support_call?) @rules = rules # @type [Array] @marked_ranges = [] @@ -101,7 +101,7 @@ def load filename, level = :normal source = Solargraph::Source.load(filename) rules = Rules.new(level, {}) api_map = Solargraph::ApiMap.new(loose_unions: - !rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_support_call?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end @@ -115,7 +115,7 @@ def load_string code, filename = nil, level = :normal, api_map: nil source = Solargraph::Source.load_string(code, filename) rules = Rules.new(level, {}) api_map ||= Solargraph::ApiMap.new(loose_unions: - !rules.require_all_unique_types_match_expected_on_lhs?) + !rules.require_all_unique_types_support_call?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end diff --git a/lib/solargraph/type_checker/rules.rb b/lib/solargraph/type_checker/rules.rb index dd341caf2..bc6826421 100644 --- a/lib/solargraph/type_checker/rules.rb +++ b/lib/solargraph/type_checker/rules.rb @@ -68,8 +68,8 @@ def require_all_unique_types_match_declared? report?(:require_all_unique_types_match_declared, :alpha) end - def require_all_unique_types_match_expected_on_lhs? - report?(:require_all_unique_types_match_expected_on_lhs, :alpha) + def require_all_unique_types_support_call? + report?(:require_all_unique_types_support_call, :alpha) end def require_no_undefined_args? From 0ccfcb0cfeb8053e616714faec492f1a79dce5d1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 20:54:23 -0500 Subject: [PATCH 470/524] Update RuboCop todo file --- .rubocop_todo.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7ed4a62b1..e8e361e57 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -106,7 +106,6 @@ Layout/EndOfLine: Exclude: - 'Gemfile' - 'Rakefile' - - 'lib/solargraph/source/encoding_fixes.rb' - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). @@ -484,7 +483,6 @@ Metrics/ParameterLists: - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/type_checker.rb' - 'lib/solargraph/yard_map/mapper/to_method.rb' - - 'lib/solargraph/yard_map/to_method.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: From c8f4751f3b01c520b4c4fc5e4eae98675552c187 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 20:56:35 -0500 Subject: [PATCH 471/524] Update RuboCop todo file --- .rubocop_todo.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b949ae2da..53776bdc0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -107,7 +107,6 @@ Layout/EndOfLine: Exclude: - 'Gemfile' - 'Rakefile' - - 'lib/solargraph/source/encoding_fixes.rb' - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). @@ -1174,7 +1173,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' From 4f16771b4cd69100672936b4bf4a663f64954bcd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 21:22:15 -0500 Subject: [PATCH 472/524] Ratchet rubocop TODO file --- .rubocop_todo.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 95dbfc1da..c907fdf5d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -107,7 +107,6 @@ Layout/EndOfLine: Exclude: - 'Gemfile' - 'Rakefile' - - 'lib/solargraph/source/encoding_fixes.rb' - 'solargraph.gemspec' # This cop supports safe autocorrection (--autocorrect). @@ -1178,7 +1177,6 @@ Style/StringLiterals: # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: Exclude: - - 'lib/solargraph/pin/base_variable.rb' - 'lib/solargraph/pin/callable.rb' - 'lib/solargraph/pin/method.rb' - 'lib/solargraph/pin/signature.rb' From 7ab85147dd588146c0ffed047daff15003fe12f8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 21:57:12 -0500 Subject: [PATCH 473/524] Move to skip: --- spec/workspace/gemspecs_resolve_require_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index 4ffcd0bd1..64a4293db 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -219,9 +219,7 @@ def configure_bundler_spec stub_value let(:require) { 'bundler/gem_tasks' } - xit 'returns gems' do - pending('improved logic for require lookups') - + it 'returns gems', skip: 'needs improved logic for require lookups' do expect(specs&.map(&:name)).to include('bundler') end end From 3f1d1e5179354140d1755fdd566e8d232faf1283 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 22:23:48 -0500 Subject: [PATCH 474/524] Mark spec as pending --- spec/api_map/index_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/api_map/index_spec.rb b/spec/api_map/index_spec.rb index 8afb74759..689d5e007 100644 --- a/spec/api_map/index_spec.rb +++ b/spec/api_map/index_spec.rb @@ -56,6 +56,8 @@ end it 'overrides #initialize method in signature' do + pending 'initialize fix' + method_pin = output_pins.find { |pin| pin.path == 'Foo#initialize' } first_parameter = method_pin.parameters.first expect(first_parameter.return_type.tag).to eq('String') From 71f609c7000500ab07b205f33a40a6819048dcf1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 22:36:54 -0500 Subject: [PATCH 475/524] Revert spec change --- spec/convention/gemfile_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/convention/gemfile_spec.rb b/spec/convention/gemfile_spec.rb index ab6a5ef1b..827da7993 100644 --- a/spec/convention/gemfile_spec.rb +++ b/spec/convention/gemfile_spec.rb @@ -24,7 +24,7 @@ def type_checker code it 'finds bad arguments to DSL methods' do checker = type_checker(%( - source 123 + source File gemspec bad_name: 'solargraph' @@ -35,7 +35,7 @@ def type_checker code expect(checker.problems.map(&:message).sort) .to eq(['Unrecognized keyword argument bad_name to Bundler::Dsl#gemspec', - 'Wrong argument type for Bundler::Dsl#source: source expected String, received 123'].sort) + 'Wrong argument type for Bundler::Dsl#source: source expected String, received Class'].sort) end it 'finds bad arguments to DSL ruby method' do From ba4f63eb2a8ee3d95f007d2cb93ea89672ee01b1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 5 Jan 2026 22:49:05 -0500 Subject: [PATCH 476/524] Drop old workaround --- .github/workflows/plugins.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index a12711e40..c7ad72cb4 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -55,8 +55,6 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 - rubygems: latest bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: From 3296a3cea121af43d185620ce8334f6b5a0545a7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 6 Jan 2026 19:25:38 -0500 Subject: [PATCH 477/524] Fix merge --- lib/solargraph/complex_type.rb | 4 ++-- lib/solargraph/complex_type/unique_type.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 9967dd2e4..8af3632d4 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -368,9 +368,9 @@ def intersect_with intersection_type, api_map items.each do |ut| intersection_type.each do |int_type| if ut.conforms_to?(api_map, int_type, :assignment) - types << int_type - elsif int_type.conforms_to?(api_map, ut, :assignment) types << ut + elsif int_type.conforms_to?(api_map, ut, :assignment) + types << int_type end end end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index feba4d7b0..36ef8d3fb 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -136,9 +136,9 @@ def intersect_with intersection_type, api_map items.each do |ut| intersection_type.each do |int_type| if ut.conforms_to?(api_map, int_type, :assignment) - types << int_type - elsif int_type.conforms_to?(api_map, ut, :assignment) types << ut + elsif int_type.conforms_to?(api_map, ut, :assignment) + types << int_type end end end From f376a9bc7d1a5c1f7e6b43c374874cca801face8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 6 Jan 2026 22:57:05 -0500 Subject: [PATCH 478/524] Fix typechecking issues --- lib/solargraph/workspace/gemspecs.rb | 2 -- sig/shims/ast/0/node.rbs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index f832cf57f..8db13a491 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -72,11 +72,9 @@ def fetch_dependencies gemspec, out: $stderr Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}" dep = Gem.loaded_specs[spec.name] # @todo is next line necessary? - # @sg-ignore Unresolved call to requirement on Gem::Dependency dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement) deps.merge fetch_dependencies(dep) if deps.add?(dep) rescue Gem::MissingSpecError - # @sg-ignore Unresolved call to requirement on Gem::Dependency Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for " \ "#{gemspec.name} not found in RubyGems." end.to_a diff --git a/sig/shims/ast/0/node.rbs b/sig/shims/ast/0/node.rbs index fab1a4de0..a9d8e06e1 100644 --- a/sig/shims/ast/0/node.rbs +++ b/sig/shims/ast/0/node.rbs @@ -1,5 +1,5 @@ module ::AST class Node - def children: () -> [self, Integer, String, Symbol, nil] + def children: () -> Array[self | Integer | String | Symbol | nil] end end From 3c947d299df4adc9dc0943ecdb2b796dd201446b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 08:04:06 -0500 Subject: [PATCH 479/524] Revert doc --- lib/solargraph/yard_map/mapper.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/yard_map/mapper.rb b/lib/solargraph/yard_map/mapper.rb index 74584d603..592b3805e 100644 --- a/lib/solargraph/yard_map/mapper.rb +++ b/lib/solargraph/yard_map/mapper.rb @@ -39,11 +39,9 @@ def generate_pins code_object @namespace_pins[code_object.path] = nspin result.push nspin if code_object.is_a?(YARD::CodeObjects::ClassObject) and !code_object.superclass.nil? - # This method of superclass detection is a bit of a - # hack. If the superclass is a Proxy that can't be - # resolved', it is assumed to be undefined in its yardoc - # and converted to a fully qualified namespace. - # + # This method of superclass detection is a bit of a hack. If + # the superclass is a Proxy, it is assumed to be undefined in its + # yardoc and converted to a fully qualified namespace. superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy) "::#{code_object.superclass}" else From 19b28ada7e4a12c4634c2c4a576134c7b1af317f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 08:07:09 -0500 Subject: [PATCH 480/524] Fix spelling --- lib/solargraph/pin/parameter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index 08b371129..e4d5b474a 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -206,7 +206,7 @@ def param_tag def typify_block_param api_map block_pin = closure if block_pin.is_a?(Pin::Block) && block_pin.receiver && index - # @sg-ignore flow-sensivie typing should handle is_a? with && + # @sg-ignore flow-sensitive typing should handle is_a? with && return block_pin.typify_parameters(api_map)[index] end ComplexType::UNDEFINED From 6b5d2c3204e4ea29278ecf510b3965545e708eec Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 08:10:18 -0500 Subject: [PATCH 481/524] Fix merge issue --- lib/solargraph/shell.rb | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 42a027144..d6420a26e 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -350,25 +350,5 @@ def print_pin(pin) puts pin.inspect end end - - # @param type [ComplexType] - # @return [void] - def print_type(type) - if options[:rbs] - puts type.to_rbs - else - puts type.rooted_tag - end - end - - # @param pin [Solargraph::Pin::Base] - # @return [void] - def print_pin(pin) - if options[:rbs] - puts pin.to_rbs - else - puts pin.inspect - end - end end end From b826d83af8a652c2dd796336399c443091bc2826 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 19:00:59 -0500 Subject: [PATCH 482/524] Exclude the current gemspec from pins brought in from gem --- lib/solargraph/workspace/gemspecs.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index fd8a9f1d6..6f8e3166d 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -232,7 +232,7 @@ def all_gemspecs_from_this_bundle if specish_objects.first.respond_to?(:materialize_for_installation) specish_objects = specish_objects.map(&:materialize_for_installation) end - specish_objects.map do |specish| + all_gemspecs = specish_objects.map do |specish| if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir) # duck type is good enough for outside uses! specish @@ -240,6 +240,13 @@ def all_gemspecs_from_this_bundle to_gem_specification(specish) end end.compact + # @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + all_gemspecs.reject do |gemspec| + gemspec.respond_to?(:source) && + gemspec.source.instance_of?(Bundler::Source::Gemspec) && + gemspec.source.respond_to?(:path) && + gemspec.source.path == '.' + end end # @return [Array] From c9520024e5b59d543ea0310dc5d95b991d2e1d34 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 19:08:57 -0500 Subject: [PATCH 483/524] Check pathname instead --- lib/solargraph/workspace/gemspecs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 6f8e3166d..3e473679e 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -245,7 +245,7 @@ def all_gemspecs_from_this_bundle gemspec.respond_to?(:source) && gemspec.source.instance_of?(Bundler::Source::Gemspec) && gemspec.source.respond_to?(:path) && - gemspec.source.path == '.' + gemspec.source.path == Pathname.new('.') end end From bcebe89f34b798b3ff4842517432da46ea6f8096 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 7 Jan 2026 19:48:28 -0500 Subject: [PATCH 484/524] Add sg-ignore --- lib/solargraph/parser/parser_gem/node_chainer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index d8d46319b..d5651f3ad 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -126,6 +126,7 @@ def generate_links n end end elsif n.type == :hash + # @sg-ignore Too many arguments to Hash.new result.push Chain::Hash.new('::Hash', n, hash_is_splatted?(n)) elsif n.type == :array chained_children = n.children.map { |c| NodeChainer.chain(c) } From 267c44534e4f9bfe6889354ff75f7aa13ec131f6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 8 Jan 2026 19:51:11 -0500 Subject: [PATCH 485/524] Avoid rbs pollution We were using the sig/shims directory for some internally helpful shims; unfortunately that exported them during gem installs, causing https://github.com/castwide/solargraph/issues/1144 --- {sig => rbs}/shims/ast/0/node.rbs | 0 {sig => rbs}/shims/ast/2.4/.rbs_meta.yaml | 0 {sig => rbs}/shims/ast/2.4/ast.rbs | 0 {sig => rbs}/shims/parser/3.2.0.1/builders/default.rbs | 0 {sig => rbs}/shims/parser/3.2.0.1/manifest.yaml | 0 {sig => rbs}/shims/parser/3.2.0.1/parser.rbs | 0 {sig => rbs}/shims/parser/3.2.0.1/polyfill.rbs | 0 {sig => rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml | 0 {sig => rbs}/shims/thor/1.2.0.1/manifest.yaml | 0 {sig => rbs}/shims/thor/1.2.0.1/thor.rbs | 0 rbs_collection.yaml | 2 +- 11 files changed, 1 insertion(+), 1 deletion(-) rename {sig => rbs}/shims/ast/0/node.rbs (100%) rename {sig => rbs}/shims/ast/2.4/.rbs_meta.yaml (100%) rename {sig => rbs}/shims/ast/2.4/ast.rbs (100%) rename {sig => rbs}/shims/parser/3.2.0.1/builders/default.rbs (100%) rename {sig => rbs}/shims/parser/3.2.0.1/manifest.yaml (100%) rename {sig => rbs}/shims/parser/3.2.0.1/parser.rbs (100%) rename {sig => rbs}/shims/parser/3.2.0.1/polyfill.rbs (100%) rename {sig => rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml (100%) rename {sig => rbs}/shims/thor/1.2.0.1/manifest.yaml (100%) rename {sig => rbs}/shims/thor/1.2.0.1/thor.rbs (100%) diff --git a/sig/shims/ast/0/node.rbs b/rbs/shims/ast/0/node.rbs similarity index 100% rename from sig/shims/ast/0/node.rbs rename to rbs/shims/ast/0/node.rbs diff --git a/sig/shims/ast/2.4/.rbs_meta.yaml b/rbs/shims/ast/2.4/.rbs_meta.yaml similarity index 100% rename from sig/shims/ast/2.4/.rbs_meta.yaml rename to rbs/shims/ast/2.4/.rbs_meta.yaml diff --git a/sig/shims/ast/2.4/ast.rbs b/rbs/shims/ast/2.4/ast.rbs similarity index 100% rename from sig/shims/ast/2.4/ast.rbs rename to rbs/shims/ast/2.4/ast.rbs diff --git a/sig/shims/parser/3.2.0.1/builders/default.rbs b/rbs/shims/parser/3.2.0.1/builders/default.rbs similarity index 100% rename from sig/shims/parser/3.2.0.1/builders/default.rbs rename to rbs/shims/parser/3.2.0.1/builders/default.rbs diff --git a/sig/shims/parser/3.2.0.1/manifest.yaml b/rbs/shims/parser/3.2.0.1/manifest.yaml similarity index 100% rename from sig/shims/parser/3.2.0.1/manifest.yaml rename to rbs/shims/parser/3.2.0.1/manifest.yaml diff --git a/sig/shims/parser/3.2.0.1/parser.rbs b/rbs/shims/parser/3.2.0.1/parser.rbs similarity index 100% rename from sig/shims/parser/3.2.0.1/parser.rbs rename to rbs/shims/parser/3.2.0.1/parser.rbs diff --git a/sig/shims/parser/3.2.0.1/polyfill.rbs b/rbs/shims/parser/3.2.0.1/polyfill.rbs similarity index 100% rename from sig/shims/parser/3.2.0.1/polyfill.rbs rename to rbs/shims/parser/3.2.0.1/polyfill.rbs diff --git a/sig/shims/thor/1.2.0.1/.rbs_meta.yaml b/rbs/shims/thor/1.2.0.1/.rbs_meta.yaml similarity index 100% rename from sig/shims/thor/1.2.0.1/.rbs_meta.yaml rename to rbs/shims/thor/1.2.0.1/.rbs_meta.yaml diff --git a/sig/shims/thor/1.2.0.1/manifest.yaml b/rbs/shims/thor/1.2.0.1/manifest.yaml similarity index 100% rename from sig/shims/thor/1.2.0.1/manifest.yaml rename to rbs/shims/thor/1.2.0.1/manifest.yaml diff --git a/sig/shims/thor/1.2.0.1/thor.rbs b/rbs/shims/thor/1.2.0.1/thor.rbs similarity index 100% rename from sig/shims/thor/1.2.0.1/thor.rbs rename to rbs/shims/thor/1.2.0.1/thor.rbs diff --git a/rbs_collection.yaml b/rbs_collection.yaml index 898239cac..d94e1c896 100644 --- a/rbs_collection.yaml +++ b/rbs_collection.yaml @@ -2,7 +2,7 @@ sources: - type: local name: shims - path: sig/shims + path: rbs/shims - type: git name: ruby/gem_rbs_collection From a5ecc28cc3b985a2fab5b7c3165026c67e1c4d5f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 8 Jan 2026 19:57:34 -0500 Subject: [PATCH 486/524] Test with RBS 4.0.0.dev.5 --- .github/workflows/rspec.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index c82ade49b..485827130 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -22,19 +22,14 @@ jobs: strategy: matrix: ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0'] - rbs-version: ['3.6.1', '3.9.5', '4.0.0.dev.4'] - # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 + rbs-version: ['3.6.1', '3.9.5', '4.0.0.dev.5'] exclude: - ruby-version: '3.0' rbs-version: '3.9.5' - - ruby-version: '3.0' - rbs-version: '4.0.0.dev.4' # Missing require in 'rbs collection update' - hopefully # fixed in next RBS release - ruby-version: '4.0' rbs-version: '3.6.1' - - ruby-version: '4.0' - rbs-version: '4.0.0.dev.4' steps: - uses: actions/checkout@v3 - name: Set up Ruby From 47d749b0c57166fb3dfe1a1a61cb4331eb1c8870 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 8 Jan 2026 20:05:47 -0500 Subject: [PATCH 487/524] Open up in gemspec --- solargraph.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solargraph.gemspec b/solargraph.gemspec index 98f524d4d..0461d93fa 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -42,7 +42,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'open3', '~> 0.2.1' s.add_runtime_dependency 'parser', '~> 3.0' s.add_runtime_dependency 'prism', '~> 1.4' - s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4'] + s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.5'] s.add_runtime_dependency 'reverse_markdown', '~> 3.0' s.add_runtime_dependency 'rubocop', '~> 1.76' s.add_runtime_dependency 'thor', '~> 1.0' From 0a724ef46c0ae47b066b931a831a57ba08a87ea6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 8 Jan 2026 20:06:47 -0500 Subject: [PATCH 488/524] Fix missing spot --- .github/workflows/rspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 0ceae2bf8..fb7feec76 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -61,7 +61,7 @@ jobs: - ruby-version: '3.3' rbs-version: '3.10.0' - ruby-version: '3.4' - rbs-version: '4.0.0.dev.4' + rbs-version: '4.0.0.dev.5' - ruby-version: '4.0' rbs-version: '4.0.0.dev.5' steps: From 2b7a7869799ea8321b13cf59f020b3cb35df4843 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 8 Jan 2026 20:22:42 -0500 Subject: [PATCH 489/524] Typecheck using RBS prereleases --- .github/workflows/linting.yml | 2 +- .github/workflows/plugins.yml | 8 ++++---- .github/workflows/typecheck.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b4ef26bfe..d8a627d1e 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -37,7 +37,7 @@ jobs: - name: Update to best available RBS run: | - bundle update rbs # use latest available for this Ruby version + bundle update --pre rbs # use latest available for this Ruby version - name: Restore cache of gem annotations id: dot-cache-restore diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index c7ad72cb4..79d95088b 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -34,7 +34,7 @@ jobs: echo 'gem "solargraph-rails"' > .Gemfile echo 'gem "solargraph-rspec"' >> .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -64,7 +64,7 @@ jobs: run: | echo 'gem "solargraph-rails"' > .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -93,7 +93,7 @@ jobs: run: | echo 'gem "solargraph-rspec"' >> .Gemfile bundle install - bundle update rbs + bundle update --pre rbs - name: Configure to use plugins run: | bundle exec solargraph config @@ -191,7 +191,7 @@ jobs: cd ../solargraph-rails echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile bundle install - bundle update rbs + bundle update --pre rbs RAILS_DIR="$(pwd)/spec/rails7" export RAILS_DIR cd ${RAILS_DIR} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index f40977acf..4fe129c22 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -32,7 +32,7 @@ jobs: - name: Install gems run: | bundle install - bundle update rbs # use latest available for this Ruby version + bundle update --pre rbs # use latest available for this Ruby version - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self From 44543d40927eb9c6db3661c9cae7ec2a50230c53 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 9 Jan 2026 09:18:41 -0500 Subject: [PATCH 490/524] Move point of ignoring cached gems for gem projects --- lib/solargraph/doc_map.rb | 12 ++++++++++++ lib/solargraph/parser/parser_gem/node_chainer.rb | 1 - lib/solargraph/workspace/gemspecs.rb | 9 +-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 87ab7ce89..932a8740e 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -138,6 +138,18 @@ def load_serialized_gem_pins out: @out # @type [Array] gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies(out: out).to_a + # if we are type checking a gem project, we should not include + # pins from rbs or yard from that gem here - we use our own + # parser for those pins + + # @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + gemspecs.reject! do |gemspec| + gemspec.respond_to?(:source) && + gemspec.source.instance_of?(Bundler::Source::Gemspec) && + gemspec.source.respond_to?(:path) && + gemspec.source.path == Pathname.new('.') + end + missing_paths.each do |path| # this will load from disk if needed; no need to manage # uncached_gemspecs to trigger that later diff --git a/lib/solargraph/parser/parser_gem/node_chainer.rb b/lib/solargraph/parser/parser_gem/node_chainer.rb index d5651f3ad..d8d46319b 100644 --- a/lib/solargraph/parser/parser_gem/node_chainer.rb +++ b/lib/solargraph/parser/parser_gem/node_chainer.rb @@ -126,7 +126,6 @@ def generate_links n end end elsif n.type == :hash - # @sg-ignore Too many arguments to Hash.new result.push Chain::Hash.new('::Hash', n, hash_is_splatted?(n)) elsif n.type == :array chained_children = n.children.map { |c| NodeChainer.chain(c) } diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 3e473679e..670fbc956 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -68,7 +68,7 @@ def resolve_require require end # look ourselves just in case this is hanging out somewhere - # that find_by_path doesn't index' + # that find_by_path doesn't index gemspec = all_gemspecs.find do |spec| spec = to_gem_specification(spec) unless spec.respond_to?(:files) @@ -240,13 +240,6 @@ def all_gemspecs_from_this_bundle to_gem_specification(specish) end end.compact - # @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] - all_gemspecs.reject do |gemspec| - gemspec.respond_to?(:source) && - gemspec.source.instance_of?(Bundler::Source::Gemspec) && - gemspec.source.respond_to?(:path) && - gemspec.source.path == Pathname.new('.') - end end # @return [Array] From c4a91f4149256225999d6de9fce7334e4a6523c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 13:12:16 -0500 Subject: [PATCH 491/524] Fix issues resolving cgi escape functions --- lib/solargraph/rbs_map.rb | 24 +++++++++++++++--------- lib/solargraph/rbs_map/stdlib_map.rb | 7 +++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index a5b998ff0..170100e9f 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -109,19 +109,19 @@ def cache_key # @param rbs_collection_config_path [String, Pathname, nil] # @return [RbsMap] 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) + # prefers stdlib RBS if available + rbs_map = RbsMap::StdlibMap.new(gemspec.name) return rbs_map if rbs_map.resolved? - # try any version of the gem in the collection - rbs_map = RbsMap.new(gemspec.name, nil, + 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? - StdlibMap.new(gemspec.name) + # 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 # @param out [IO, nil] where to log messages @@ -187,6 +187,13 @@ def conversions # @return [void] def log_caching lib, out:; end + def resolve_dependencies? + # we need to resolve dependencies via gemfile.lock manually for + # YARD regardless, so use same mechanism here so we don't + # duplicate work generating pins from dependencies + false + end + # @param loader [RBS::EnvironmentLoader] # @param library [String] # @param version [String, nil] the version of the library to load, or nil for any @@ -194,8 +201,7 @@ def log_caching lib, out:; end # @return [Boolean] true if adding the library succeeded def add_library loader, library, version, out: $stderr @resolved = if loader.has_library?(library: library, version: version) - # we find our own dependencies from gemfile.lock - loader.add library: library, version: version, resolve_dependencies: false + loader.add library: library, version: version, resolve_dependencies: resolve_dependencies? logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } true else diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index e7891bfe3..6c008954e 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -54,6 +54,13 @@ def self.stdlib_dependencies name, version = nil end end + def resolve_dependencies? + # there are 'virtual' dependencies for stdlib gems in RBS that + # aren't represented in the actual gemspecs that we'd + # otherwise use + true + end + # @param library [String] # @return [StdlibMap] def self.load library From 02151a52d49659aa3fe919b35cc143bf9ddc1d76 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 13:33:30 -0500 Subject: [PATCH 492/524] Be more careful marking things as stdlib --- lib/solargraph/rbs_map.rb | 8 ++++---- lib/solargraph/rbs_map/stdlib_map.rb | 2 +- spec/pin_cache_spec.rb | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 170100e9f..a42f984ad 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -204,10 +204,10 @@ def add_library loader, library, version, out: $stderr loader.add library: library, version: version, resolve_dependencies: resolve_dependencies? logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" } true - else - logger.info { "#{short_name} did not find data for library #{library}:#{version}" } - false - end + else + logger.info { "#{short_name} did not find data for library #{library}:#{version}" } + false + end end # @return [String] diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index 6c008954e..c2cfcabb6 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -25,7 +25,7 @@ def initialize library, out: $stderr @resolved = true @loaded = true logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" } - else + elsif self.class.source.has? library, nil super unless resolved? @pins = [] diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb index 0a11686f5..0f766117a 100644 --- a/spec/pin_cache_spec.rb +++ b/spec/pin_cache_spec.rb @@ -136,8 +136,8 @@ end end - context 'with gem packaged with its own RBS gem' do - let(:gem_name) { 'base64' } + context 'with gem packaged with its own RBS' do + let(:gem_name) { 'rubocop-yard' } before do Solargraph::Shell.new.uncache(gem_name) @@ -150,7 +150,7 @@ pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) # match arguments with regexp using rspec-matchers syntax - expect(File).to have_received(:write).with(%r{combined/.*/base64-.*-export.ser$}, any_args, mode: 'wb').once + expect(File).to have_received(:write).with(%r{combined/.*/rubocop-yard-.*-export.ser$}, any_args, mode: 'wb').once end end end From 14a84689739ec6b0547d4a5586411d71fa4f434b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 13:45:13 -0500 Subject: [PATCH 493/524] Reclassify rbs gem --- spec/rbs_map/stdlib_map_spec.rb | 24 ------------------------ spec/rbs_map_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/spec/rbs_map/stdlib_map_spec.rb b/spec/rbs_map/stdlib_map_spec.rb index 03f0b547f..c9db9bb48 100644 --- a/spec/rbs_map/stdlib_map_spec.rb +++ b/spec/rbs_map/stdlib_map_spec.rb @@ -11,30 +11,6 @@ expect(pin).to be_a(Solargraph::Pin::Base) end - it 'processes RBS class variables' do - map = Solargraph::RbsMap::StdlibMap.load('rbs') - store = Solargraph::ApiMap::Store.new(map.pins) - class_variable_pins = store.pins_by_class(Solargraph::Pin::ClassVariable) - count_pins = class_variable_pins.select do |pin| - pin.name.to_s == '@@count' && pin.context.to_s == 'Class' - end - expect(count_pins.length).to eq(1) - count_pin = count_pins.first - expect(count_pin.return_type.to_s).to eq('Integer') - end - - it 'processes RBS class instance variables' do - map = Solargraph::RbsMap::StdlibMap.load('rbs') - store = Solargraph::ApiMap::Store.new(map.pins) - instance_variable_pins = store.pins_by_class(Solargraph::Pin::InstanceVariable) - root_pins = instance_variable_pins.select do |pin| - pin.name.to_s == '@root' && pin.context.to_s == 'Class' && pin.scope == :class - end - expect(root_pins.length).to eq(1) - root_pin = root_pins.first - expect(root_pin.return_type.to_s).to eq('RBS::Namespace, nil') - end - it 'processes RBS module aliases' do map = Solargraph::RbsMap::StdlibMap.load('yaml') store = Solargraph::ApiMap::Store.new(map.pins) diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index f3ca90a36..4631c9ca5 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -25,4 +25,30 @@ pin = rbs_map.path_pin('RBS::EnvironmentWalker::InstanceNode') expect(pin.return_type.tag).to eq('Class') end + + it 'processes RBS class variables' do + spec = Gem::Specification.find_by_name('rbs') + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + store = Solargraph::ApiMap::Store.new(rbs_map.pins) + class_variable_pins = store.pins_by_class(Solargraph::Pin::ClassVariable) + count_pins = class_variable_pins.select do |pin| + pin.name.to_s == '@@count' && pin.context.to_s == 'Class' + end + expect(count_pins.length).to eq(1) + count_pin = count_pins.first + expect(count_pin.return_type.to_s).to eq('Integer') + end + + it 'processes RBS class instance variables' do + spec = Gem::Specification.find_by_name('rbs') + rbs_map = Solargraph::RbsMap.from_gemspec(spec, nil, nil) + store = Solargraph::ApiMap::Store.new(rbs_map.pins) + instance_variable_pins = store.pins_by_class(Solargraph::Pin::InstanceVariable) + root_pins = instance_variable_pins.select do |pin| + pin.name.to_s == '@root' && pin.context.to_s == 'Class' && pin.scope == :class + end + expect(root_pins.length).to eq(1) + root_pin = root_pins.first + expect(root_pin.return_type.to_s).to eq('RBS::Namespace, nil') + end end From d5668fe8f5aa0f7fdc05d6bbd8ccf0bfc431a07f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 14:16:56 -0500 Subject: [PATCH 494/524] Fix merge --- spec/doc_map_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index ca03438fc..7c541da27 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -102,11 +102,6 @@ end end - it 'collects dependencies' do - doc_map = Solargraph::DocMap.new(['rspec'], workspace) - expect(doc_map.dependencies.map(&:name)).to include('rspec-core') - end - context 'with an uncached but valid gemspec' do let(:requires) { ['uncached_gem'] } let(:pre_cache) { false } From cde107371b6fd4c7447c437a6e520b50ed65d7bb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 15:22:22 -0500 Subject: [PATCH 495/524] Add sg-ignore --- lib/solargraph/pin/callable.rb | 4 +++- lib/solargraph/yardoc.rb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index edbc3f941..cb867d785 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -205,7 +205,9 @@ 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 + # @todo this and its caller should be changed so that this can + # look at the kwargs provided and check names against what + # we acccept return false if argcount < parcount && !(argcount == parcount - 1 && parameters.last.restarg?) true end diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 907afb2de..084bb38f4 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -32,6 +32,8 @@ def cache(yard_plugins, gemspec) yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" } Solargraph.logger.debug { "Running: #{cmd}" } # @todo set these up to run in parallel + # @sg-ignore Our fill won't work properly due to an issue in + # Callable#arity_matches? - see comment there stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir) unless status.success? Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" } From e155ffab81f98ed38da9ab6bf5a29f38a89aae2a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 15:32:13 -0500 Subject: [PATCH 496/524] Fix merge --- lib/solargraph/pin/callable.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index cb867d785..732bca768 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -205,6 +205,7 @@ 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 # @todo this and its caller should be changed so that this can # look at the kwargs provided and check names against what # we acccept From d8dfcc02ed0967b5ff82ecf7200b2b0042ff4c1e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 16:13:22 -0500 Subject: [PATCH 497/524] Remove outdated workaround --- .github/workflows/plugins.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 0a83f8aba..9a9b80b70 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -55,8 +55,6 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 - rubygems: latest bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: From 0fad08f752f36470bfd72bf11880a850d3e8bad5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 16:46:35 -0500 Subject: [PATCH 498/524] Fix @sg-ignore name --- lib/solargraph/language_server/host.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index d43f58a80..44d1ecdeb 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -301,7 +301,7 @@ def prepare directory, name = nil end # @return [String] - # @sg-ignore Need detailed hash types + # @sg-ignore Need to validate config def command_path options['commandPath'] || 'solargraph' end From 8656254513aa466947d66ed32f061b3be67a8848 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 17:15:04 -0500 Subject: [PATCH 499/524] Restore workaround --- .github/workflows/plugins.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 9a9b80b70..2631b70cc 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -56,6 +56,8 @@ jobs: with: ruby-version: 3.4 bundler-cache: false + # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 + rubygems: latest - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq From 236734ed404d265d71a9b7191613876a61132e59 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 17:25:20 -0500 Subject: [PATCH 500/524] Restore workaround --- .github/workflows/plugins.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 2631b70cc..166db358b 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -56,8 +56,6 @@ jobs: with: ruby-version: 3.4 bundler-cache: false - # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 - rubygems: latest - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq @@ -176,6 +174,8 @@ jobs: ruby-version: '3.0' bundler-cache: false bundler: latest + # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 + rubygems: latest env: MATRIX_RAILS_VERSION: "7.0" - name: Install gems From a575014fd316ede2ef17ca1167f62ce10f68f9f8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 19:01:46 -0500 Subject: [PATCH 501/524] Merge branch 'flow_sensitive_typing_2_0' into 2025-01-06 --- lib/solargraph/complex_type/type_methods.rb | 2 +- lib/solargraph/complex_type/unique_type.rb | 12 ++++++------ lib/solargraph/equality.rb | 2 +- lib/solargraph/gem_pins.rb | 1 - .../parser/parser_gem/node_processors/send_node.rb | 3 +-- lib/solargraph/pin/parameter.rb | 1 - lib/solargraph/rbs_map/conversions.rb | 2 +- lib/solargraph/type_checker.rb | 4 ---- 8 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index aeeb98f06..df7522c70 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -206,7 +206,7 @@ def scope # @param other [Object] def == other return false unless self.class == other.class - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore Flow-sensitive typing should support .class == .class tag == other.tag end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4d6ca1387..c018bd544 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -182,17 +182,17 @@ def determine_non_literal_name def eql?(other) self.class == other.class && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @name == other.name && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @key_types == other.key_types && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @subtypes == other.subtypes && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @rooted == other.rooted? && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @all_params == other.all_params && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class @parameters_type == other.parameters_type end diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index f8c50ff31..69f415e26 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -12,7 +12,7 @@ module Equality # @return [Boolean] def eql?(other) self.class.eql?(other.class) && - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support .class == .class equality_fields.eql?(other.equality_fields) end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index 15103952f..790422065 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -57,7 +57,6 @@ def self.combine(yard_pins, rbs_pins) next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method) unless rbs_pin - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 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 diff --git a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb index 861d6b157..55d5e58d2 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -221,6 +221,7 @@ def process_module_function node: ref.node, source: :parser) pins.push mm, cm + # @param ivar [Pin::InstanceVariable] pins.select{|pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path}.each do |ivar| pins.delete ivar pins.push Solargraph::Pin::InstanceVariable.new( @@ -228,7 +229,6 @@ def process_module_function closure: cm, name: ivar.name, comments: ivar.comments, - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 assignment: ivar.assignment, source: :parser ) @@ -237,7 +237,6 @@ def process_module_function closure: mm, name: ivar.name, comments: ivar.comments, - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 assignment: ivar.assignment, source: :parser ) diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index c5f86a6ea..bec8f6294 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -251,7 +251,6 @@ def param_tag def typify_block_param api_map block_pin = closure if block_pin.is_a?(Pin::Block) && block_pin.receiver && index - # @sg-ignore flow-sensitive typing should handle is_a? with && return block_pin.typify_parameters(api_map)[index] end ComplexType::UNDEFINED diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index ceeb5454b..739542516 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -65,7 +65,7 @@ def convert_decl_to_pin decl, closure # STDERR.puts "Skipping interface #{decl.name.relative!}" interface_decl_to_pin decl, closure when RBS::AST::Declarations::TypeAlias - # @sg-ignore https://github.com/castwide/solargraph/pull/1114 + # @sg-ignore flow-sensitive typing should support case/when type_aliases[decl.name.to_s] = decl when RBS::AST::Declarations::Module module_decl_to_pin decl diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 0b9eb7e00..dcddc23c6 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -332,11 +332,8 @@ def call_problems all_closest = all_found.map { |pin| pin.typify(api_map) } closest = ComplexType.new(all_closest.flat_map(&:items).uniq) # @todo remove the internal_or_core? check at a higher-than-strict level - # @sg-ignore Change to something flow-sensitive typing understands if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found)) - # @sg-ignore Change to something flow-sensitive typing understands unless closest.generic? || ignored_pins.include?(found) - # @sg-ignore Change to something flow-sensitive typing understands if closest.defined? result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") else @@ -685,7 +682,6 @@ def declared_externally? pin end all_closest = all_found.map { |pin| pin.typify(api_map) } closest = ComplexType.new(all_closest.flat_map(&:items).uniq) - # @sg-ignore Change to something flow-sensitive typing understands if !found || closest.defined? || internal?(found) return false end From 16506e8f50d2a9ef23e8c8cc0f195ebc86151006 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 19:02:14 -0500 Subject: [PATCH 502/524] Fix method signature --- lib/solargraph/workspace.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 252fc5148..d0c3cb970 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -236,10 +236,11 @@ def rbs_collection_config_path # @param name [String] # @param version [String, nil] + # @param out [IO, nil] # # @return [Gem::Specification, nil] - def find_gem name, version = nil - gemspecs.find_gem(name, version) + def find_gem name, version = nil, out: nil + gemspecs.find_gem(name, version, out: out) end # @return [Array] From 740db4e7772129aae2d225baf5c3549d969a40a0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 19:03:03 -0500 Subject: [PATCH 503/524] Fix annotations --- lib/solargraph/library.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 7de06bacd..ca6253a17 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -590,7 +590,6 @@ def cache_errors def cache_next_gemspec return if @cache_progress - # @type [Gem::Specification] spec = cacheable_specs.first return end_cache_progress unless spec From 4d1c21a76943ad06057d8832379a5f2854c2195b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 19:51:21 -0500 Subject: [PATCH 504/524] Add regression test and fix for issue found during future merge --- lib/solargraph/source/chain.rb | 2 +- spec/type_checker/levels/alpha_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index f7a03b552..7aa8dede1 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -276,7 +276,7 @@ def infer_from_definitions pins, context, api_map, locals return type end - type.self_to_type(context.return_type) + type.self_to_type(context.context) end # @param type [ComplexType] diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 781907d33..2abe5ab7c 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -73,5 +73,25 @@ def catalog expect(checker.problems.map(&:message)).to eq([]) end + + it 'resolves self correctly in arguments' do + checker = type_checker(%( + class Foo + # @param other [self] + # + # @return [String] + def bar other + other.bing + end + + # @return [String] + def bing + 'bing' + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end From 29da3afe0647121035c1d5384f7b8cf10d30b97a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 10 Jan 2026 20:33:27 -0500 Subject: [PATCH 505/524] Add regression test and fix for issue found during future merge --- lib/solargraph/source/chain.rb | 2 +- spec/type_checker/levels/alpha_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 7aa8dede1..e9722fcc4 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -271,7 +271,7 @@ def infer_from_definitions pins, context, api_map, locals else ComplexType.new(types) end - if context.nil? || context.return_type.undefined? + if context.nil? || context.context.undefined? # up to downstream to resolve self type return type end diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 2abe5ab7c..b9acfc83a 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -93,5 +93,26 @@ def bing expect(checker.problems.map(&:message)).to eq([]) end + + it 'resolves self correctly in arguments' do + checker = type_checker(%( + class Blah + # @return [String] + attr_reader :filename + + # @param filename [String] + def initialize filename + @filename = filename + end + + # @param location [self] + def contain? location + filename == location.filename + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end From b7b31a9e1746a01c8dffacbe5aca898c57f67d8a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 10:56:41 -0500 Subject: [PATCH 506/524] Fix merge --- lib/solargraph/pin/base_variable.rb | 8 -------- lib/solargraph/pin/local_variable.rb | 24 ------------------------ 2 files changed, 32 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 60fd5a392..c2f29edd9 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -276,14 +276,6 @@ def combine_closure(other) other.closure end - # @param other_closure [Pin::Closure] - # @param other_loc [Location] - def visible_at?(other_closure, other_loc) - location.filename == other_loc.filename && - (!presence || presence.include?(other_loc.range.start)) && - visible_in_closure?(other_closure) - end - def presence_certain? @presence_certain end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index b287bbfea..b7b0cf5f1 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -22,30 +22,6 @@ def combine_with(other, attrs={}) super end - # @param other [self] - # @return [Pin::Closure, nil] - def combine_closure(other) - return closure if self.closure == other.closure - - # choose first defined, as that establishes the scope of the variable - if closure.nil? || other.closure.nil? - Solargraph.assert_or_log(:varible_closure_missing) do - "One of the local variables being combined is missing a closure: " \ - "#{self.inspect} vs #{other.inspect}" - end - return closure || other.closure - end - - if closure.location.nil? || other.closure.location.nil? - return closure.location.nil? ? other.closure : closure - end - - # if filenames are different, this will just pick one - return closure if closure.location <= other.closure.location - - other.closure - end - # @param other_closure [Pin::Closure] # @param other_loc [Location] def visible_at?(other_closure, other_loc) From 9ac6e9319352f8fae58efa3e96de687825ae3a63 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 10:57:39 -0500 Subject: [PATCH 507/524] Fix merge --- lib/solargraph/pin/base_variable.rb | 8 ++++++++ lib/solargraph/pin/local_variable.rb | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index c2f29edd9..60fd5a392 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -276,6 +276,14 @@ def combine_closure(other) other.closure end + # @param other_closure [Pin::Closure] + # @param other_loc [Location] + def visible_at?(other_closure, other_loc) + location.filename == other_loc.filename && + (!presence || presence.include?(other_loc.range.start)) && + visible_in_closure?(other_closure) + end + def presence_certain? @presence_certain end diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index b7b0cf5f1..9841a9fd3 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -22,14 +22,6 @@ def combine_with(other, attrs={}) super end - # @param other_closure [Pin::Closure] - # @param other_loc [Location] - def visible_at?(other_closure, other_loc) - location.filename == other_loc.filename && - (!presence || presence.include?(other_loc.range.start)) && - visible_in_closure?(other_closure) - end - def to_rbs (name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped') end From e0deadf2f665e6589ac5f1938a15abbd77a06d78 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 17:14:56 -0500 Subject: [PATCH 508/524] Use correct field for self type resolution Add a regression test and fix for self type resolution issue found on a future branch --- lib/solargraph/source/chain.rb | 2 +- spec/type_checker/levels/alpha_spec.rb | 29 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 spec/type_checker/levels/alpha_spec.rb diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index f7a03b552..7aa8dede1 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -276,7 +276,7 @@ def infer_from_definitions pins, context, api_map, locals return type end - type.self_to_type(context.return_type) + type.self_to_type(context.context) end # @param type [ComplexType] diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb new file mode 100644 index 000000000..118f4cccf --- /dev/null +++ b/spec/type_checker/levels/alpha_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe Solargraph::TypeChecker do + context 'when at alpha level' do + def type_checker code + Solargraph::TypeChecker.load_string(code, 'test.rb', :alpha) + end + + it 'resolves self correctly in arguments' do + checker = type_checker(%( + class Foo + # @param other [self] + # + # @return [String] + def bar other + other.bing + end + + # @return [String] + def bing + 'bing' + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end + end +end From dbcd966d5c3f350effbd2a8058a57f111c95bbea Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 17:49:37 -0500 Subject: [PATCH 509/524] Fix 'solargraph pin --references ClassName' private method call --- lib/solargraph/api_map.rb | 12 ++++++------ spec/shell_spec.rb | 32 +++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index cc3031ea5..6c0da316e 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -711,6 +711,12 @@ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scop methods end + # @param fq_sub_tag [String] + # @return [String, nil] + def qualify_superclass fq_sub_tag + store.qualify_superclass fq_sub_tag + end + private # A hash of source maps with filename keys. @@ -804,12 +810,6 @@ def path_macros @path_macros ||= {} end - # @param fq_sub_tag [String] - # @return [String, nil] - def qualify_superclass fq_sub_tag - store.qualify_superclass fq_sub_tag - end - # Get the namespace's type (Class or Module). # # @param fqns [String] A fully qualified namespace diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index b9dc6b327..3b8dc0426 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -57,7 +57,37 @@ def bundle_exec(*cmd) end end - describe 'pin' do + describe 'pin on a class' do + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:string_pin) { instance_double(Solargraph::Pin::Namespace, name: 'String') } + + before do + allow(Solargraph::ApiMap).to receive(:load_with_cache).and_return(api_map) + allow(Solargraph::Pin::Namespace).to receive(:===).with(string_pin).and_return(true) + allow(string_pin).to receive(:return_type).and_return(Solargraph::ComplexType.parse('String')) + allow(api_map).to receive(:get_path_pins).with('String').and_return([string_pin]) + end + + context 'with --references option' do + let(:object_pin) { instance_double(Solargraph::Pin::Namespace, name: 'Object') } + + before do + allow(Solargraph::Pin::Namespace).to receive(:===).with(object_pin).and_return(true) + allow(api_map).to receive(:qualify_superclass).with('String').and_return('Object') + allow(api_map).to receive(:get_path_pins).with('Object').and_return([object_pin]) + end + + it 'prints a pin with info' do + out = capture_both do + shell.options = { references: true } + shell.pin('String') + end + expect(out).to include('# Superclass:') + end + end + end + + describe 'pin on a method' do let(:api_map) { instance_double(Solargraph::ApiMap) } let(:to_s_pin) { instance_double(Solargraph::Pin::Method, return_type: Solargraph::ComplexType.parse('String')) } From 4f4b6a05b6154f8d6461248f6f47705dc478c9e7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 17:58:09 -0500 Subject: [PATCH 510/524] Add error handling --- lib/solargraph/shell.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index d6420a26e..9983f82cf 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -132,6 +132,8 @@ def uncache *gems end spec = workspace.find_gem(gem) + raise Thor::InvocationError, "Gem '#{gem}' not found" if spec.nil? + PinCache.uncache_gem(spec, out: $stdout) end end From 176b50649e01ccbeb0013bc439093515f3d30760 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 18:05:51 -0500 Subject: [PATCH 511/524] Fix another location with another test case --- lib/solargraph/source/chain.rb | 2 +- spec/type_checker/levels/alpha_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 7aa8dede1..e9722fcc4 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -271,7 +271,7 @@ def infer_from_definitions pins, context, api_map, locals else ComplexType.new(types) end - if context.nil? || context.return_type.undefined? + if context.nil? || context.context.undefined? # up to downstream to resolve self type return type end diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 118f4cccf..4fd591fda 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -25,5 +25,26 @@ def bing expect(checker.problems.map(&:message)).to eq([]) end + + it 'resolves self correctly in arguments (second case)' do + checker = type_checker(%( + class Blah + # @return [String] + attr_reader :filename + + # @param filename [String] + def initialize filename + @filename = filename + end + + # @param location [self] + def contain? location + filename == location.filename + end + end + )) + + expect(checker.problems.map(&:message)).to eq([]) + end end end From 45ea8955b5c0c3c95332489cbb93f5529690d9f0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 18:19:34 -0500 Subject: [PATCH 512/524] Drop now-unneeded @sg-ignore --- lib/solargraph/pin/method.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 86bf1cd09..e6e0f6503 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -92,7 +92,6 @@ def combine_with(other, attrs = {}) end new_attrs = { visibility: combine_visibility(other), - # @sg-ignore https://github.com/castwide/solargraph/pull/1050 explicit: explicit? || other.explicit?, block: combine_blocks(other), node: choose_node(other, :node), From 92286c13d17ec6b2209762b9c808abc4cf71e1a6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 18:34:20 -0500 Subject: [PATCH 513/524] Don't log caching for each dependent library This causes duplicate logging on standard libraries, many of which are esoteric (e.g., "cgi-escaping"). The current method as of the 2025-01-06 branch would result in each stdlib library being cached individually. --- lib/solargraph/rbs_map.rb | 4 ++-- lib/solargraph/rbs_map/stdlib_map.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index a42f984ad..04a718454 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -128,7 +128,7 @@ def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path # @return [Array] def pins out: $stderr @pins ||= if resolved? - loader.libs.each { |lib| log_caching(lib, out: out) } + log_caching(library, out: out) conversions.pins else [] @@ -182,7 +182,7 @@ def conversions @conversions ||= Conversions.new(loader: loader) end - # @param lib [RBS::EnvironmentLoader::Library] + # @param lib [String] # @param out [IO, nil] where to log messages # @return [void] def log_caching lib, out:; end diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index c2cfcabb6..0f812150c 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -13,7 +13,7 @@ class StdlibMap < RbsMap @stdlib_maps_hash = {} def log_caching lib, out: $stderr - out&.puts("Caching RBS pins for standard library #{lib.name}") + out&.puts("Caching RBS pins for standard library #{lib}") end # @param library [String] From 2e316d59d8cf02f95bc060e5384f15b5ece2edc4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 18:49:03 -0500 Subject: [PATCH 514/524] Drop logging entirely --- lib/solargraph/rbs_map.rb | 6 ------ lib/solargraph/rbs_map/stdlib_map.rb | 4 ---- 2 files changed, 10 deletions(-) diff --git a/lib/solargraph/rbs_map.rb b/lib/solargraph/rbs_map.rb index 04a718454..ca03f71e2 100644 --- a/lib/solargraph/rbs_map.rb +++ b/lib/solargraph/rbs_map.rb @@ -128,7 +128,6 @@ def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path # @return [Array] def pins out: $stderr @pins ||= if resolved? - log_caching(library, out: out) conversions.pins else [] @@ -182,11 +181,6 @@ def conversions @conversions ||= Conversions.new(loader: loader) end - # @param lib [String] - # @param out [IO, nil] where to log messages - # @return [void] - def log_caching lib, out:; end - def resolve_dependencies? # we need to resolve dependencies via gemfile.lock manually for # YARD regardless, so use same mechanism here so we don't diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index 0f812150c..ebfdadb61 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -12,10 +12,6 @@ class StdlibMap < RbsMap # @type [Hash{String => RbsMap}] @stdlib_maps_hash = {} - def log_caching lib, out: $stderr - out&.puts("Caching RBS pins for standard library #{lib}") - end - # @param library [String] # @param out [IO, nil] where to log messages def initialize library, out: $stderr From 458efed2e4591949052c47073932887dc6b7e6b9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 20:36:13 -0500 Subject: [PATCH 515/524] Fix some types based on future branch feedback --- lib/solargraph/workspace/gemspecs.rb | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 20fe350ae..e44103c58 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -94,11 +94,11 @@ def stdlib_dependencies stdlib_name # # @return [Gem::Specification, nil] def find_gem name, version = nil, out: $stderr - gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name && gemspec.version == version } - return gemspec if gemspec + specish = all_gemspecs_from_bundle.find { |specish| specish.name == name && specish.version == version } + return to_gem_specification specish if specish - gemspec = all_gemspecs_from_bundle.find { |gemspec| gemspec.name == name } - return gemspec if gemspec + specish = all_gemspecs_from_bundle.find { |specish| specish.name == name } + return to_gem_specification specish if specish resolve_gem_ignoring_local_bundle name, version, out: out end @@ -116,7 +116,6 @@ def fetch_dependencies gemspec, out: $stderr # @param runtime_dep [Gem::Dependency] # @param deps [Hash{String => Gem::Specification}] gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| - # @sg-ignore Unresolved call to requirement on Gem::Dependency dep = find_gem(runtime_dep.name, runtime_dep.requirement) next unless dep @@ -155,6 +154,7 @@ def self.gem_specification_cache private # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] + # # @return [Gem::Specification, nil] def to_gem_specification specish # print time including milliseconds @@ -162,6 +162,7 @@ def to_gem_specification specish when Gem::Specification @@warned_on_rubygems ||= false if specish.respond_to?(:identifier) + # @type [Gem::Specification] specish else # see https://github.com/castwide/solargraph/actions/runs/17588131738/job/49961580698?pr=1006 - happened on Ruby 3.0 @@ -181,11 +182,12 @@ def to_gem_specification specish when Bundler::StubSpecification # turns a Bundler::StubSpecification into a # Gem::StubSpecification into a Gem::Specification - # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' specish = specish.stub - # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' if specish.respond_to?(:spec) - # @sg-ignore Flow-sensitive typing ought to be able to handle 'when ClassName' + # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' + # @type [Gem::Specification] specish.spec else # turn the crank again @@ -328,6 +330,7 @@ def all_gemspecs_from_external_bundle 'specish_objects = specish_objects.map(&:materialize_for_installation);' \ 'end;' \ 'specish_objects.map { |specish| [specish.name, specish.version] }' + # @type [Array] query_external_bundle(command).map do |name, version| resolve_gem_ignoring_local_bundle(name, version) end.compact From 45e5eca22dd540deda022fc28bd87728925d4c1f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 21:20:39 -0500 Subject: [PATCH 516/524] Provide Gem::Specification to outside interface --- lib/solargraph/workspace/gemspecs.rb | 28 ++++--------------- .../gemspecs_resolve_require_spec.rb | 8 ++---- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index e44103c58..030707db7 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -116,6 +116,7 @@ def fetch_dependencies gemspec, out: $stderr # @param runtime_dep [Gem::Dependency] # @param deps [Hash{String => Gem::Specification}] gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps| + # @sg-ignore Unresolved call to requirement on Gem::Dependency dep = find_gem(runtime_dep.name, runtime_dep.requirement) next unless dep @@ -155,25 +156,12 @@ def self.gem_specification_cache # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] # - # @return [Gem::Specification, nil] + # @return [Gem::Specification] def to_gem_specification specish # print time including milliseconds self.class.gem_specification_cache[specish] ||= case specish when Gem::Specification - @@warned_on_rubygems ||= false - if specish.respond_to?(:identifier) - # @type [Gem::Specification] - specish - else - # see https://github.com/castwide/solargraph/actions/runs/17588131738/job/49961580698?pr=1006 - happened on Ruby 3.0 - unless @@warned_on_rubygems - logger.warn "Incomplete Gem::Specification encountered - recommend upgrading rubygems" - @@warned_on_rubygems = true - end - nil - end - # yay! - + specish when Bundler::LazySpecification # materializing didn't work. Let's look in the local # rubygems without bundler's help @@ -194,13 +182,9 @@ def to_gem_specification specish to_gem_specification(specish) end else - @@warned_on_gem_type ||= false - unless @@warned_on_gem_type - # @sg-ignore Unresolved call to class on Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification - logger.warn "Unexpected type while resolving gem: #{specish.class}" - @@warned_on_gem_type = true - end - nil + # @sg-ignore Unresolved call to class on Gem::Specification, Bundler::LazySpecification, + # Bundler::StubSpecification + raise "Unexpected type while resolving gem: #{specish.class}" end end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index d53638600..e13166bf3 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -71,12 +71,8 @@ def install_gem gem_name, version allow(lockfile).to receive(:to_s).and_return(dir_path) end - it 'returns a single spec' do - expect(specs.size).to eq(1) - end - - it 'resolves to the right known gem' do - expect(specs.map(&:name)).to eq(['solargraph']) + it 'raises a StandardException' do + expect { specs.size }.to raise_error(StandardError) end end From 2d5456a28a88c27276d2db5411067a21f126bb51 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 21:25:38 -0500 Subject: [PATCH 517/524] Provide Gem::Specification to outside interface --- lib/solargraph/workspace/gemspecs.rb | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 030707db7..9feba7524 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -169,21 +169,12 @@ def to_gem_specification specish specish.version when Bundler::StubSpecification # turns a Bundler::StubSpecification into a - # Gem::StubSpecification into a Gem::Specification + # Gem::StubSpecification + to_gem_specification specish.stub + when Gem::StubSpecification # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' - specish = specish.stub - # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' - if specish.respond_to?(:spec) - # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' - # @type [Gem::Specification] - specish.spec - else - # turn the crank again - to_gem_specification(specish) - end + specish.spec else - # @sg-ignore Unresolved call to class on Gem::Specification, Bundler::LazySpecification, - # Bundler::StubSpecification raise "Unexpected type while resolving gem: #{specish.class}" end end From 4ce641d4cec01d174c688dd9ea6d0aa7cca70a2c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 21:33:24 -0500 Subject: [PATCH 518/524] Use #to_spec --- lib/solargraph/workspace/gemspecs.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 9feba7524..7494f96e0 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -172,8 +172,7 @@ def to_gem_specification specish # Gem::StubSpecification to_gem_specification specish.stub when Gem::StubSpecification - # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName' - specish.spec + specish.to_spec else raise "Unexpected type while resolving gem: #{specish.class}" end From aa99710e02aad4e476c665a3bddb696ed75c3cf3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 21:57:41 -0500 Subject: [PATCH 519/524] Provide Gem::Specification to outside interface --- lib/solargraph/workspace/gemspecs.rb | 13 ++++++++++--- spec/workspace/gemspecs_resolve_require_spec.rb | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 7494f96e0..e15f4da90 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -156,7 +156,7 @@ def self.gem_specification_cache # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification] # - # @return [Gem::Specification] + # @return [Gem::Specification, nil] def to_gem_specification specish # print time including milliseconds self.class.gem_specification_cache[specish] ||= case specish @@ -169,8 +169,15 @@ def to_gem_specification specish specish.version when Bundler::StubSpecification # turns a Bundler::StubSpecification into a - # Gem::StubSpecification - to_gem_specification specish.stub + # Gem::StubSpecification if we can + if specish.respond_to?(:stub) + to_gem_specification specish.stub + else + # A Bundler::StubSpecification is a Bundler:: + # RemoteSpecification which ought to proxy a Gem:: + # Specification + specish + end when Gem::StubSpecification specish.to_spec else diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index e13166bf3..8deba9ff8 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -92,6 +92,7 @@ def configure_bundler_spec stub_value allow(bundler_stub_spec).to receive(:respond_to?).with(:version).and_return(true) allow(bundler_stub_spec).to receive(:respond_to?).with(:gem_dir).and_return(false) allow(bundler_stub_spec).to receive(:respond_to?).with(:materialize_for_installation).and_return(false) + allow(bundler_stub_spec).to receive(:respond_to?).with(:stub).and_return(false) allow(bundler_stub_spec).to receive_messages(name: 'solargraph', stub: stub_value) end From d9188e7f14e16c383333e221ebe5381852401922 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 22:08:37 -0500 Subject: [PATCH 520/524] Fix typechecking error --- lib/solargraph/workspace/gemspecs.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index e15f4da90..feddf641b 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -178,6 +178,7 @@ def to_gem_specification specish # Specification specish end + # @sg-ignore Unresolved constant Gem::StubSpecification when Gem::StubSpecification specish.to_spec else From 8eea21f8bbe6f735ef9915095a597d192af937a6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 22:17:29 -0500 Subject: [PATCH 521/524] Use consistent bundler versions --- .github/workflows/linting.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ae885b4db..faeb330bf 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -31,9 +31,8 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler: latest bundler-cache: true - cache-version: 2025-06-06 + cache-version: 2026-01-11 - name: Update to best available RBS run: | From ad6ee19005d45389fe1f9fc9952111971dff4e2e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 22:43:01 -0500 Subject: [PATCH 522/524] Fix type issue --- lib/solargraph/workspace/gemspecs.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 7af70d474..3bf4edb73 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -182,6 +182,7 @@ def to_gem_specification specish # Specification specish end + # @sg-ignore Unresolved constant Gem::StubSpecification when Gem::StubSpecification specish.to_spec else From f88875cf426d29a294482a682886a3b712d66518 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 23:12:39 -0500 Subject: [PATCH 523/524] Fix annotations based on future branch feedback --- lib/solargraph/pin_cache.rb | 4 ++-- lib/solargraph/workspace.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 97f5f5d6a..c60a71234 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -27,7 +27,7 @@ def cached? gemspec combined_gem?(gemspec, rbs_version_cache_key) end - # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param gemspec [Gem::Specification] # @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists # @param out [StringIO, IO, nil] output stream for logging # @return [void] @@ -167,7 +167,7 @@ def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key: [build_yard, build_rbs_collection, build_combined] end - # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param gemspec [Gem::Specification] # @param rbs_version_cache_key [String, nil] # @param build_yard [Boolean] # @param build_rbs_collection [Boolean] diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 61ea253d6..ee9707477 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -73,7 +73,7 @@ def global_environ @global_environ ||= Convention.for_global(DocMap.new([], [], self)) end - # @param gemspec [Gem::Specification, Bundler::LazySpecification] + # @param gemspec [Gem::Specification] # @param out [StringIO, IO, nil] output stream for logging # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # From 9268eae5bd58e4462aa3c499f58624ec489addc2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 11 Jan 2026 23:17:54 -0500 Subject: [PATCH 524/524] Add some @todos --- lib/solargraph/doc_map.rb | 1 + lib/solargraph/rbs_map/stdlib_map.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 891bbabb3..cf5c40e6b 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -145,6 +145,7 @@ def load_serialized_gem_pins out: @out # try to resolve the stdlib name deps = workspace.stdlib_dependencies(stdlib_name_guess) || [] [stdlib_name_guess, *deps].compact.each do |potential_stdlib_name| + # @todo Need to support splatting in literal array rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name serialized_pins.concat rbs_pins if rbs_pins end diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index c2cfcabb6..fab2b3222 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -48,6 +48,8 @@ def self.source # @return [Array String}>, nil] def self.stdlib_dependencies name, version = nil if source.has?(name, version) + # @todo we are relying on undocumented behavior where + # passing version=nil gives the latest version it has source.dependencies_of(name, version) else []