From 10ef2a80252eacd1c49c47c482a074ac918cd6d4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 20 Mar 2025 16:56:05 -0400 Subject: [PATCH 001/333] 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 df1334b7ff02bfaa637ba2fe180c3a8ea966f6da Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 6 May 2025 12:39:50 -0400 Subject: [PATCH 002/333] Enable strict type checking in CI --- .github/workflows/typecheck.yml | 2 +- Rakefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 5b1b5e151..8f9119592 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -31,4 +31,4 @@ jobs: - name: Install gems run: bundle install - name: Typecheck self - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strict diff --git a/Rakefile b/Rakefile index 33b91bfa4..a7fea9b13 100755 --- a/Rakefile +++ b/Rakefile @@ -15,7 +15,7 @@ end desc "Run the type checker" task :typecheck do - sh "bundle exec solargraph typecheck --level typed" + sh "bundle exec solargraph typecheck --level strict" end desc "Run all tests" From 5bc55d306911fb2f40e9b56699264dc76aec1d7e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 1 Jun 2025 19:31:10 -0400 Subject: [PATCH 003/333] 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 8b1107501abeb326df1f8fecf3030f8f4b632b9e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 13 Jul 2025 12:44:19 -0400 Subject: [PATCH 004/333] 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 bf612959ec8b43b888dac18b4ee6823af5e6ffc5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 14 Jul 2025 07:27:58 -0400 Subject: [PATCH 005/333] 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 006/333] 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 007/333] 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 008/333] 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 009/333] 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 010/333] 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 011/333] 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 012/333] 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 013/333] 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 014/333] 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 015/333] 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 016/333] 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 017/333] 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 018/333] 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 019/333] 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 020/333] 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 021/333] 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 022/333] 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 023/333] 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 024/333] 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 025/333] 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 026/333] 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 027/333] 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 028/333] 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 029/333] 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 030/333] 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 031/333] 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 032/333] 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 033/333] 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 034/333] 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 035/333] 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 5799b5213e8d7d6d2f3d680d7010367a1fc03956 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 27 Aug 2025 09:00:00 -0400 Subject: [PATCH 036/333] 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 037/333] 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 038/333] 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 039/333] 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 040/333] 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 041/333] 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 042/333] 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 043/333] 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 044/333] 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 045/333] 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 046/333] 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 047/333] 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 048/333] 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 049/333] 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 050/333] 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 051/333] 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 052/333] 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 053/333] 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 054/333] 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 055/333] 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 056/333] 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 057/333] 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 058/333] 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 059/333] 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 060/333] 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 061/333] 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 062/333] 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 063/333] 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 064/333] 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 065/333] 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 066/333] 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 067/333] 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 068/333] 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 447c778642edfef4adc62d2af3526f730417342c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 29 Aug 2025 12:05:14 -0400 Subject: [PATCH 069/333] Make 'self' types concrete while checking arguments --- lib/solargraph/type_checker.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index e0b23f56b..dd13d7ec7 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -411,6 +411,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) + argtype = argtype.self_to_type(closure_pin.context) 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 @@ -450,8 +451,10 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, # @todo Some level (strong, I guess) should require the param here else ptype = data[:qualified] + ptype = ptype.self_to_type(pin.context) unless ptype.undefined? argtype = argchain.infer(api_map, block_pin, locals) + argtype = argtype.self_to_type(block_pin.context) 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 @@ -477,7 +480,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 && !any_types_match?(api_map, ptype, argtype) result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}") end From 36383596fa21593bb6db0f7fffa343b37141aa75 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 29 Aug 2025 12:51:12 -0400 Subject: [PATCH 070/333] strict -> strong --- .github/workflows/typecheck.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 4cde97763..f40977acf 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -18,7 +18,7 @@ permissions: jobs: solargraph_typed: - name: Solargraph / typed + name: Solargraph / strong runs-on: ubuntu-latest @@ -36,4 +36,4 @@ jobs: - name: Install gem types run: bundle exec rbs collection install - name: Typecheck self - run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strict \ No newline at end of file + run: SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong From b4a2ab1443bab5bc083339ffcdbc23d81c463959 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 29 Aug 2025 12:53:22 -0400 Subject: [PATCH 071/333] Also change default in Rakefile --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 0e20d1280..c83d9ab6b 100755 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ task :console do end desc "Run the type checker" -task typecheck: [:typecheck_strict] +task typecheck: [:typecheck_strong] desc "Run the type checker at typed level - return code issues provable without annotations being correct" task :typecheck_typed do From 6acfa0c54a806da203daf30765c7a99a67066f16 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 30 Aug 2025 09:06:14 -0400 Subject: [PATCH 072/333] 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 073/333] 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 91ced058c5c81f3b1e63c51b27c08f3ca14c2339 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 30 Aug 2025 21:25:08 -0400 Subject: [PATCH 074/333] Add spec --- spec/type_checker/levels/strong_spec.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index a03e6eb5d..94f0f207f 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -97,6 +97,29 @@ def bar expect(checker.problems.map(&:message)).to include('Call to #foo is missing keyword argument b') end + it 'understands complex use of other' do + 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') From 2702f443f84884bc5a72d84a3da66f5d7df51a51 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 30 Aug 2025 21:39:22 -0400 Subject: [PATCH 075/333] Work around strong typechecking issue --- lib/solargraph/type_checker.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index dd13d7ec7..2f9b7da1c 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -453,8 +453,7 @@ def kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, ptype = data[:qualified] ptype = ptype.self_to_type(pin.context) unless ptype.undefined? - argtype = argchain.infer(api_map, block_pin, locals) - argtype = argtype.self_to_type(block_pin.context) + argtype = argchain.infer(api_map, block_pin, locals).self_to_type(block_pin.context) 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 From f623a73ad88f44d3ff6e6873a1682f33a07d431b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 1 Sep 2025 07:52:39 -0400 Subject: [PATCH 076/333] 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 077/333] 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 078/333] 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 079/333] 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 080/333] 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 081/333] 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 082/333] 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 b10cdd19a1936c13b504540cda4ce527b4faf0c4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 4 Sep 2025 08:36:01 -0400 Subject: [PATCH 083/333] Drop broken 'namespaces' method These unused methods call into ApiMap::Index#namespaces, which does not exist. --- lib/solargraph/api_map.rb | 7 ------- lib/solargraph/api_map/store.rb | 5 ----- 2 files changed, 12 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index f58633a0c..c44ceea93 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -236,13 +236,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 diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index f3e2ed278..a4148f867 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -142,11 +142,6 @@ def namespace_exists?(fqns) fqns_pins(fqns).any? end - # @return [Set] - def namespaces - index.namespaces - end - # @return [Enumerable] def namespace_pins pins_by_class(Solargraph::Pin::Namespace) From bb0f6079024f1e28ee9f12c26bbc626fe475d2ce Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 14:18:16 -0400 Subject: [PATCH 084/333] 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 41f098b9245c194b5367d0e521fd595082118ee6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 6 Sep 2025 14:41:54 -0400 Subject: [PATCH 085/333] Rebuild rubocop todo file --- .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 086/333] 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 087/333] 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 088/333] 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 089/333] 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 090/333] 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 837d7f67872f47619200320bafb7d80da9abad95 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 7 Sep 2025 16:57:05 -0400 Subject: [PATCH 091/333] 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 092/333] 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 093/333] 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 094/333] 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 095/333] 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 096/333] 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 097/333] 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 098/333] 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 099/333] 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 100/333] 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 101/333] 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 102/333] 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 103/333] 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 104/333] 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 105/333] 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 106/333] 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 107/333] 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 db725f78fdb3756c917044e35dd6d1eec1c86095 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 13 Sep 2025 16:30:39 -0400 Subject: [PATCH 108/333] 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 109/333] 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 110/333] 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 df2573539463c71815ed97ee4ed1d3347a03cf64 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 14 Sep 2025 20:45:04 -0400 Subject: [PATCH 111/333] 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 112/333] 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 113/333] 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 114/333] 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 e5d88a51307a948de85880fcbbde4a8ec790d199 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 19 Sep 2025 18:15:17 -0400 Subject: [PATCH 115/333] 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 3399448f739d09b0a1297e878fac5499ff9bb816 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 23 Sep 2025 20:59:58 -0400 Subject: [PATCH 116/333] 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 117/333] 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 118/333] 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 119/333] 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 120/333] 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 f4399eba824fdd5aa8d605b2143ddef20c5617d8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 11:15:45 -0400 Subject: [PATCH 121/333] [regression] Fix issue resolving mixins under same namespace --- lib/solargraph/api_map/constants.rb | 7 ++++- spec/api_map/constants_spec.rb | 36 +++++++++++++++++++++++++ spec/type_checker/levels/strong_spec.rb | 20 ++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 430303ae1..51d0a207c 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -120,8 +120,13 @@ def complex_resolve name, gates, internal return [resolved, gates[(idx + 1)..]] if resolved store.get_ancestor_references(gate).each do |ref| return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") - mixin = resolve(ref.name, ref.reference_gates - gates) + + # avoid infinite loops resolving mixin pin + next if ref.name == name && gates.to_set == ref.reference_gates.to_set + + mixin = resolve(ref.name, ref.reference_gates) next unless mixin + resolved = simple_resolve(name, mixin, internal) return [resolved, gates[(idx + 1)..]] if resolved end diff --git a/spec/api_map/constants_spec.rb b/spec/api_map/constants_spec.rb index 26eaf6b25..c0460e79a 100644 --- a/spec/api_map/constants_spec.rb +++ b/spec/api_map/constants_spec.rb @@ -20,6 +20,42 @@ module Quuz expect(resolved).to eq('Foo::Bar') end + it 'resolves straightforward mixins' do + source_map = Solargraph::SourceMap.load_string(%( + module Bar + Baz = 'baz' + end + + class Foo + include Bar + end + ), 'test.rb') + + store = Solargraph::ApiMap::Store.new(source_map.pins) + constants = described_class.new(store) + pin = source_map.first_pin('Foo') + resolved = constants.resolve('Baz', pin.gates) + expect(resolved).to eq('Bar::Baz') + end + + it 'resolves mixin living under same namespace' do + source_map = Solargraph::SourceMap.load_string(%( + class Foo + module Bar + Baz = 'baz' + end + + include Bar + end + ), 'test.rb') + + store = Solargraph::ApiMap::Store.new(source_map.pins) + constants = described_class.new(store) + pin = source_map.first_pin('Foo') + resolved = constants.resolve('Baz', pin.gates) + expect(resolved).to eq('Foo::Bar::Baz') + end + it 'returns namespaces for nested namespaces' do source_map = Solargraph::SourceMap.load_string(%( module Foo diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index a03e6eb5d..4bf3b7163 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -272,5 +272,25 @@ def meth arg )) expect(checker.problems).to be_empty end + + it 'resolves constants inside modules inside classes' do + checker = type_checker(%( + class Bar + module Foo + CONSTANT = 'hi' + end + end + + class Bar + include Foo + + # @return [String] + def baz + CONSTANT + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end end end From 3324b4c7fb392405c39252701a9494d70d63dcf0 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 12:11:22 -0400 Subject: [PATCH 122/333] Prevent recursion via caching mechanism --- lib/solargraph/api_map/constants.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 51d0a207c..bc508b330 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -24,8 +24,16 @@ def resolve(name, *gates) return store.get_path_pins(name[2..]).first&.path if name.start_with?('::') flat = gates.flatten - flat.push '' if flat.empty? - cached_resolve[[name, flat]] || resolve_and_cache(name, flat) + if flat.empty? + flat.push '' + end + if cached_resolve.include? [name, flat] + cached_result = cached_resolve[[name, flat]] + # don't recurse + return nil if cached_result == :in_process + return cached_result + end + resolve_and_cache(name, flat) end # Get a fully qualified namespace from a reference pin. @@ -82,6 +90,7 @@ def clear # @param gates [Array] # @return [String, nil] def resolve_and_cache name, gates + cached_resolve[[name, gates]] = :in_process cached_resolve[[name, gates]] = resolve_uncached(name, gates) end @@ -121,9 +130,6 @@ def complex_resolve name, gates, internal store.get_ancestor_references(gate).each do |ref| return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") - # avoid infinite loops resolving mixin pin - next if ref.name == name && gates.to_set == ref.reference_gates.to_set - mixin = resolve(ref.name, ref.reference_gates) next unless mixin @@ -159,7 +165,7 @@ def collect_and_cache gates end end - # @return [Hash{Array(Name, Array) => String, nil}] + # @return [Hash{Array(String, Array) => String, :in_process, nil}] def cached_resolve @cached_resolve ||= {} end From 174bf4e430edf7198c9ae3d23d701e359a74146c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 12:15:46 -0400 Subject: [PATCH 123/333] Linting fix, ignore rubocop-yard issue pending yard PR merge --- .rubocop_todo.yml | 33 ++--------------------------- lib/solargraph/api_map/constants.rb | 4 +--- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 89fd47c5d..f17d08a94 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' @@ -1782,7 +1767,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 +1819,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 +1886,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' @@ -1947,7 +1929,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 +2030,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 +2110,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 +2121,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 +2147,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 +2245,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 +2260,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 +2477,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' @@ -2551,6 +2522,7 @@ YARD/MismatchName: YARD/TagTypeSyntax: Exclude: + - 'lib/solargraph/api_map/constants.rb' - 'lib/solargraph/language_server/host.rb' - 'lib/solargraph/parser/comment_ripper.rb' - 'lib/solargraph/type_checker.rb' @@ -2614,7 +2586,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/constants.rb b/lib/solargraph/api_map/constants.rb index bc508b330..0df8d83ce 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -24,9 +24,7 @@ def resolve(name, *gates) return store.get_path_pins(name[2..]).first&.path if name.start_with?('::') flat = gates.flatten - if flat.empty? - flat.push '' - end + flat.push '' if flat.empty? if cached_resolve.include? [name, flat] cached_result = cached_resolve[[name, flat]] # don't recurse From 4898313a80331277b733ce827962ff5eaadf5a16 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 16:03:39 -0400 Subject: [PATCH 124/333] [regression] Fix resolution in deep YARD namespace hierarchies YARD-parsed namespaces weren't correctly setting their gates, leading to unresolved types from methods. --- lib/solargraph/yard_map/mapper/to_namespace.rb | 1 + spec/yard_map/mapper_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/solargraph/yard_map/mapper/to_namespace.rb b/lib/solargraph/yard_map/mapper/to_namespace.rb index 054ba3306..f7063e3d6 100644 --- a/lib/solargraph/yard_map/mapper/to_namespace.rb +++ b/lib/solargraph/yard_map/mapper/to_namespace.rb @@ -21,6 +21,7 @@ def self.make code_object, spec, closure = nil type: code_object.is_a?(YARD::CodeObjects::ClassObject) ? :class : :module, visibility: code_object.visibility, closure: closure, + gates: closure.gates, source: :yardoc, ) end diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 6b00e5c33..63efc3835 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -74,6 +74,14 @@ 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 + 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", ""]) + end + it 'adds extend references' do # Asssuming the yard gem exists because it's a known dependency gemspec = Gem::Specification.find_by_name('yard') From cb47db054f16e625bba1331738376068e9899dbb Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 17:31:01 -0400 Subject: [PATCH 125/333] [regression] Fix resolution of ambiguous argument types This is a case where incorrect gates result in the wrong type being used - found by strong typechecking on a branch ::Solargraph::Pin::Symbol was resolved as ::Symbol in a generics scenario. --- lib/solargraph/pin/proxy_type.rb | 3 +- lib/solargraph/source/chain/call.rb | 5 +++- spec/source/chain/call_spec.rb | 37 +++++++++++++++++++++++++ spec/type_checker/levels/strong_spec.rb | 32 +++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index 2323489a7..6babeb353 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -5,8 +5,9 @@ module Pin class ProxyType < Base # @param return_type [ComplexType] # @param binder [ComplexType, ComplexType::UniqueType, nil] - def initialize return_type: ComplexType::UNDEFINED, binder: nil, **splat + def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: [''], **splat super(**splat) + @gates = gates @return_type = return_type @binder = binder if binder end diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 24d10656d..5dca071de 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -98,7 +98,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) + name_pin = Pin::ProxyType.anonymous(name_pin.context, + gates: name_pin.gates, + source: :chain) + atype = atypes[idx] ||= arg.infer(api_map, name_pin, locals) unless param.compatible_arg?(atype, api_map) || param.restarg? match = false break diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 8b67a3c66..3725686a7 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -627,4 +627,41 @@ 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 + 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/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index a03e6eb5d..f9ff1ebb5 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -272,5 +272,37 @@ def meth arg )) expect(checker.problems).to be_empty end + + it 'resolves class name correctly in generic resolution' do + checker = 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 + )) + expect(checker.problems.map(&:message)).to be_empty + end end end From 97e5519600f9b1d4bdf39c400f20f2cd104bf079 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 17:43:19 -0400 Subject: [PATCH 126/333] Fix gates default value to match existing behavior --- lib/solargraph/pin/proxy_type.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index 6babeb353..ecaa94fdd 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -5,7 +5,7 @@ module Pin class ProxyType < Base # @param return_type [ComplexType] # @param binder [ComplexType, ComplexType::UniqueType, nil] - def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: [''], **splat + def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat super(**splat) @gates = gates @return_type = return_type From 7840f9e91cbf0bad3d849f9e3fc565b1af25101e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 17:47:37 -0400 Subject: [PATCH 127/333] Linting --- spec/type_checker/levels/strong_spec.rb | 53 ++++++++++++++----------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index f9ff1ebb5..0991ad9ad 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -273,36 +273,41 @@ def meth arg expect(checker.problems).to be_empty end - it 'resolves class name correctly in generic resolution' do - checker = type_checker(%( - module Foo - module Bar - class Symbol + 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 - 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 + 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 - )) - expect(checker.problems.map(&:message)).to be_empty + )) + end + + it 'resolves class name correctly in generic resolution' do + expect(checker.problems.map(&:message)).to be_empty + end end end end From 99cf71d31770203c7a48685f87f1e79eb5511234 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 25 Sep 2025 18:18:47 -0400 Subject: [PATCH 128/333] Linting --- lib/solargraph/pin/proxy_type.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index ecaa94fdd..1fed841a3 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -5,6 +5,7 @@ module Pin class ProxyType < Base # @param return_type [ComplexType] # @param binder [ComplexType, ComplexType::UniqueType, nil] + # @param gates [Array, nil] def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat super(**splat) @gates = gates From dfdb952a5ac56a38adfd34f913073dfb8cee1cc2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 08:58:35 -0400 Subject: [PATCH 129/333] Fix re-used variable name issue --- lib/solargraph/pin/proxy_type.rb | 1 + lib/solargraph/source/chain/call.rb | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index 1fed841a3..452536834 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -4,6 +4,7 @@ module Solargraph module Pin class ProxyType < Base # @param return_type [ComplexType] + # @param gates [Array, nil] Namespaces to try while resolving non-rooted types # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param gates [Array, nil] def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 5dca071de..74afff4e0 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -98,10 +98,10 @@ def inferred_pins pins, api_map, name_pin, locals match = ol.parameters.any?(&:restarg?) break end - name_pin = Pin::ProxyType.anonymous(name_pin.context, - gates: name_pin.gates, - source: :chain) - atype = atypes[idx] ||= arg.infer(api_map, name_pin, locals) + arg_name_pin = Pin::ProxyType.anonymous(name_pin.context, + gates: name_pin.gates, + 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 From f8f7ca27b1dc40864a4ac49e3c26caa1fcb33b84 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 09:02:13 -0400 Subject: [PATCH 130/333] Linting --- spec/yard_map/mapper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 63efc3835..6fa778f2c 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -79,7 +79,7 @@ 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", ""]) + expect(pin.gates).to eq(['AST::Processor::Mixin', 'AST::Processor', 'AST', '']) end it 'adds extend references' do From 8ce4bb91e5f2f7a57c8125150427e67b8e465ee7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 09:26:26 -0400 Subject: [PATCH 131/333] Fix merge --- spec/yard_map/mapper_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 6fa778f2c..d45af985b 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -76,7 +76,10 @@ it 'adds corect gates' do # Asssuming the ast gem exists because it's a known dependency - pin = pins_with('ast').find do |pin| + 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.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 a398b84bd2f09c4e9148aee79c8ee0a643c651a1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 26 Sep 2025 12:45:46 -0400 Subject: [PATCH 132/333] 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 133/333] 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 134/333] 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 135/333] 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 136/333] 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 137/333] 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 138/333] 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 139/333] 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 140/333] 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 141/333] 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 142/333] 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 55cf9c32da1375e9213d656e83d865b320131c69 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 27 Sep 2025 10:12:21 -0400 Subject: [PATCH 143/333] Fix pin combination consistency issue I was hitting a strange 'works on my machine' issue and figured it out that I wasn't seeing it because of a difference in where files lived in CI vs my machine. This resulted in different pin info being selected during combination, as we were using the file location when we had no better way to prefer one over the other. This should make the result more consistent, making user and CI pin-combination-triggered issues easier to reproduce. --- 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 c0eecabb2..d11c9455e 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -354,7 +354,7 @@ def choose_pin_attr(other, attr) end # arbitrary way of choosing a pin # @sg-ignore Need _1 support - [val1, val2].compact.min_by { _1.best_location.to_s } + [val1, val2].compact.max_by { File.basename(_1.best_location.to_s) } end # @return [void] From 5d4bbc3b37c01eadfac7c60b17035e7412346e77 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 27 Sep 2025 22:09:31 -0400 Subject: [PATCH 144/333] Closure merging fixes 1) Prefer closures with more gates, to maximize compatibility 2) Look at basename of location, to make choice consistent --- lib/solargraph/pin/base.rb | 11 +++++++++-- spec/pin/base_spec.rb | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index c0eecabb2..bb6608d0f 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -353,8 +353,15 @@ def choose_pin_attr(other, attr) # :nocov: end # arbitrary way of choosing a pin - # @sg-ignore Need _1 support - [val1, val2].compact.min_by { _1.best_location.to_s } + [val1, val2].compact.max_by do |closure| + [ + # maximize number of gates, as types in other combined pins may + # depend on those gates + closure.gates.length, + # use basename so that results don't vary system to system + File.basename(closure.best_location.to_s) + ] + end end # @return [void] diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index 6bcce6cef..d1d2f0cf4 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -48,4 +48,18 @@ pin = Solargraph::Pin::Base.new(name: 'Foo', comments: '@return [undefined]') expect(pin.link_documentation).to eq('Foo') end + + it 'deals well with known closure combination issue' do + Solargraph::Shell.new.uncache('yard') + api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) + pins = api_map.get_method_stack('YARD::Docstring', 'parser', scope: :class) + expect(pins.length).to eq(1) + parser_method_pin = pins.first + expect(parser_method_pin.source).to eq(:combined) + return_type = parser_method_pin.typify(api_map) + expect(parser_method_pin.closure.name).to eq("Docstring") + expect(parser_method_pin.closure.gates).to eq(["YARD::Docstring", "YARD", '']) + expect(return_type).to be_defined + expect(parser_method_pin.typify(api_map).rooted_tags).to eq('::YARD::DocstringParser') + end end From b5898e2a97b81a0219ee145769c978d1fabcb6c3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 27 Sep 2025 22:21:02 -0400 Subject: [PATCH 145/333] Drop incidental requirement --- spec/pin/base_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index d1d2f0cf4..1a6cfd1e8 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -55,7 +55,6 @@ pins = api_map.get_method_stack('YARD::Docstring', 'parser', scope: :class) expect(pins.length).to eq(1) parser_method_pin = pins.first - expect(parser_method_pin.source).to eq(:combined) return_type = parser_method_pin.typify(api_map) expect(parser_method_pin.closure.name).to eq("Docstring") expect(parser_method_pin.closure.gates).to eq(["YARD::Docstring", "YARD", '']) From 1416e1d1e3f51c5c44dea3807796cbccccd870d3 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 28 Sep 2025 12:08:05 -0400 Subject: [PATCH 146/333] 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 147/333] 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 12e45630867530a19b8c839c162c2ada88d03511 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 28 Sep 2025 14:19:54 -0400 Subject: [PATCH 148/333] [regression] Fix resolution of a nested type case Found in the Solargraph::Pin::DelegatedMethod class in strong typechecking in https://github.com/apiology/solargraph/pull/12 --- lib/solargraph/api_map/constants.rb | 3 ++- spec/pin/parameter_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 430303ae1..3e846e0b1 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| - return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") + return ref.name.sub(/^::/, '') if ref.name.end_with?("::#{name}") && ref.name.start_with?('::') + mixin = resolve(ref.name, ref.reference_gates - gates) next unless mixin resolved = simple_resolve(name, mixin, internal) diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 082ec54c6..81d40e5db 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -473,5 +473,33 @@ def self.foo bar: 'bar' type = pin.probe(api_map) expect(type.simple_tags).to eq('String') end + + it 'infers types from kwoptarg values' do + 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 From b8174ddb186418dc0ef2feec67c514da1e977d31 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 28 Sep 2025 14:36:41 -0400 Subject: [PATCH 149/333] Fix example name --- spec/pin/parameter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pin/parameter_spec.rb b/spec/pin/parameter_spec.rb index 81d40e5db..14c39f3fe 100644 --- a/spec/pin/parameter_spec.rb +++ b/spec/pin/parameter_spec.rb @@ -474,7 +474,7 @@ def self.foo bar: 'bar' expect(type.simple_tags).to eq('String') end - it 'infers types from kwoptarg values' do + it 'handles a relative type name case' do source = Solargraph::Source.load_string(%( module A module B From faa57aa5b71898b899033ac1e49263fcecdb920b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 08:32:26 -0400 Subject: [PATCH 150/333] Add another unmerged-Yard-PR issue --- .rubocop_todo.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4745544d0..83339e756 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 @@ -1286,6 +1279,7 @@ YARD/MismatchName: 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' From b9f29005c3e270a009688e72b68ae8005e99ba33 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 09:37:05 -0400 Subject: [PATCH 151/333] 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 dfabbec1a..d8bdb4424 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -462,7 +462,7 @@ 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? - argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(block_pin.context) + argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) 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 From 3df4406de77a159e1997c10a4b7d3d7532745ab2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 11:13:47 -0400 Subject: [PATCH 152/333] Ratchet RuboCop --- .rubocop_todo.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4745544d0..3d48a2371 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 From 2e6aa3f9344c299c22223265bb6d3fd7de4ba123 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 12:25:15 -0400 Subject: [PATCH 153/333] Drop unneeded @sg-ignores --- lib/solargraph/parser/parser_gem/node_processors/send_node.rb | 3 --- 1 file changed, 3 deletions(-) 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 645baf00f..3b7ec74b1 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -36,15 +36,12 @@ def process process_autoload elsif method_name == :private_constant process_private_constant - # @sg-ignore elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym process_alias_method - # @sg-ignore elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node) # Processing a private class can potentially handle children on its own return if process_private_class_method end - # @sg-ignore elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)' pins.push Pin::Reference::Require.new(Solargraph::Location.new(region.filename, Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser) end From 66abe83bd0d6d3759aa5e9bac70eff86009f098c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 12:26:53 -0400 Subject: [PATCH 154/333] Drop unneeded @sg-ignore --- lib/solargraph/workspace/require_paths.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index 67adae9e6..c8eea161b 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -76,7 +76,6 @@ def require_path_from_gemspec_file gemspec_file_path "spec = eval(File.read('#{gemspec_file_path}'), TOPLEVEL_BINDING, '#{gemspec_file_path}'); " \ 'return unless Gem::Specification === spec; ' \ 'puts({name: spec.name, paths: spec.require_paths}.to_json)'] - # @sg-ignore Unresolved call to capture3 on Module o, e, s = Open3.capture3(*cmd) if s.success? begin From c35d6a7e74831f7c3562600b111a5c861658725d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 12:32:45 -0400 Subject: [PATCH 155/333] Drop another @sg-ignore --- lib/solargraph/pin/delegated_method.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index 9483fb058..bcf5b5912 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -51,7 +51,6 @@ def type_location %i[typify realize infer probe].each do |method| # @param api_map [ApiMap] define_method(method) do |api_map| - # @sg-ignore Unresolved call to resolve_method resolve_method(api_map) # @sg-ignore Need to set context correctly in define_method blocks @resolved_method ? @resolved_method.send(method, api_map) : super(api_map) From 7cba7ca66ac466b71f0fb939b7fa8b1ac6d23a70 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 13:46:53 -0400 Subject: [PATCH 156/333] Bump for call.rb complexity --- .rubocop_todo.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83339e756..35deed756 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: From 470c26e1c755eb5c0ebca75c2ead76c8651342e9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Tue, 30 Sep 2025 14:20:32 -0400 Subject: [PATCH 157/333] 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 158/333] 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 159/333] 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 160/333] 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 152dbdfe446088f8c997c55d663321e264bf5ed2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 17:43:51 -0400 Subject: [PATCH 161/333] Annotation fixes for strong typechecking --- lib/solargraph/api_map/index.rb | 6 ++++-- lib/solargraph/doc_map.rb | 3 +-- .../parser/parser_gem/node_processors/opasgn_node.rb | 2 ++ lib/solargraph/pin/local_variable.rb | 3 --- lib/solargraph/position.rb | 1 - lib/solargraph/source_map.rb | 4 +++- lib/solargraph/source_map/mapper.rb | 2 -- lib/solargraph/workspace/config.rb | 6 ++++++ 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 35d86446a..cb61a28eb 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -114,8 +114,10 @@ def catalog new_pins self end - # @param klass [Class] - # @param hash [Hash{String => Array}] + # @generic T + # @param klass [Class] + # @param hash [Hash{String => T}] + # # @return [void] def map_references klass, hash pins_by_class(klass).each do |pin| diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5966717f4..05f2f1647 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 @@ -180,7 +180,6 @@ def load_serialized_gem_pins # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] 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 diff --git a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb index 0e4d7b26a..a4359af9d 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb @@ -13,8 +13,10 @@ def process operator = node.children[1] argument = node.children[2] if target.type == :send + # @sg-ignore Need a downcast here process_send_target(target, operator, argument) elsif target.type.to_s.end_with?('vasgn') + # @sg-ignore Need a downcast here process_vasgn_target(target, operator, argument) else Solargraph.assert_or_log(:opasgn_unknown_target, diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 9eae6cc6f..cb2dda140 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -26,9 +26,6 @@ def combine_with(other, 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) super(other, new_attrs) diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 2faa0a99b..1197038ef 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -58,7 +58,6 @@ def inspect # @return [Integer] def self.to_offset text, position return 0 if text.empty? - # @sg-ignore Unresolved call to + on Integer text.lines[0...position.line].sum(&:length) + position.character end diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index d7b6fb4fc..15b747760 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -41,11 +41,13 @@ def initialize source # solargraph-rails is known to use this method to get the document symbols. It should probably be removed. @document_symbols = nil self.convention_pins = conventions_environ.pins + # @type [Hash{Class => Array}] @pin_select_cache = {} end # @generic T # @param klass [Class>] + # # @return [Array>] def pins_by_class klass @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten @@ -171,10 +173,10 @@ def map source private - # @return [Hash{Class => Array}] # @return [Array] attr_writer :convention_pins + # @return [Hash{Class => Array}] def pin_class_hash @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a) end diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 18fdf1f88..5fdcb9fe6 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -70,7 +70,6 @@ def closure_at(position) # @param comment [String] # @return [void] def process_comment source_position, comment_position, comment - # @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst, received Regexp return unless comment.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP cmnt = remove_inline_comment_hashes(comment) parse = Solargraph::Source.parse_docstring(cmnt) @@ -245,7 +244,6 @@ def remove_inline_comment_hashes comment # @return [void] def process_comment_directives - # @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst, received Regexp return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines @source.associated_comments.each do |line, comments| diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index d1e6c27b5..3e30e5d74 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -63,6 +63,7 @@ def calculated # namespace. It's typically used to identify available DSLs. # # @return [Array] + # @sg-ignore Need to validate config def domains raw_data['domains'] end @@ -70,6 +71,7 @@ def domains # An array of required paths to add to the workspace. # # @return [Array] + # @sg-ignore Need to validate config def required raw_data['require'] end @@ -83,6 +85,7 @@ def require_paths # An array of reporters to use for diagnostics. # + # @sg-ignore Need to validate config # @return [Array] def reporters raw_data['reporters'] @@ -90,6 +93,7 @@ def reporters # A hash of options supported by the formatter # + # @sg-ignore Need to validate config # @return [Hash] def formatter raw_data['formatter'] @@ -97,6 +101,7 @@ def formatter # An array of plugins to require. # + # @sg-ignore Need to validate config # @return [Array] def plugins raw_data['plugins'] @@ -104,6 +109,7 @@ def plugins # The maximum number of files to parse from the workspace. # + # @sg-ignore Need to validate config # @return [Integer] def max_files raw_data['max_files'] From 94e650bf298482a79e6139eedd50a21916fd0c14 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 18:01:26 -0400 Subject: [PATCH 162/333] 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 2240ad7aeb03bfc30cbda1b724b4e9616a3ba90f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 07:56:22 -0400 Subject: [PATCH 163/333] Add @sg-ignores for issues covered by future PRs --- lib/solargraph/doc_map.rb | 1 + lib/solargraph/position.rb | 1 + lib/solargraph/shell.rb | 1 + lib/solargraph/source_map/mapper.rb | 6 ++++++ lib/solargraph/type_checker.rb | 1 + 5 files changed, 10 insertions(+) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 05f2f1647..5dcf28552 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -180,6 +180,7 @@ def load_serialized_gem_pins # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] 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 diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 1197038ef..2faa0a99b 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -58,6 +58,7 @@ def inspect # @return [Integer] def self.to_offset text, position return 0 if text.empty? + # @sg-ignore Unresolved call to + on Integer text.lines[0...position.line].sum(&:length) + position.character end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..1284e16de 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -80,6 +80,7 @@ def config(directory = '.') end end File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| + # @sg-ignore Unresolved call to to_yaml file.puts conf.to_yaml end STDOUT.puts "Configuration file initialized." diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 5fdcb9fe6..871f3ed5f 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -70,6 +70,9 @@ def closure_at(position) # @param comment [String] # @return [void] def process_comment source_position, comment_position, comment + # @sg-ignore Wrong argument type for String#=~: object + # expected String::_MatchAgainst, received + # Regexp return unless comment.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP cmnt = remove_inline_comment_hashes(comment) parse = Solargraph::Source.parse_docstring(cmnt) @@ -244,6 +247,9 @@ def remove_inline_comment_hashes comment # @return [void] def process_comment_directives + # @sg-ignore Wrong argument type for String#=~: object + # expected String::_MatchAgainst, received + # Regexp return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines @source.associated_comments.each do |line, comments| diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index d8bdb4424..36263a0ce 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -463,6 +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? 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 From b0fb367852f9d4d919abb17db6fb38803cc75eb1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 08:07:55 -0400 Subject: [PATCH 164/333] Add @sg-ignores for issues covered by future PRs --- lib/solargraph/shell.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 1284e16de..a005f600b 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -80,7 +80,6 @@ def config(directory = '.') end end File.open(File.join(directory, '.solargraph.yml'), 'w') do |file| - # @sg-ignore Unresolved call to to_yaml file.puts conf.to_yaml end STDOUT.puts "Configuration file initialized." From 1a2a546f0b47549089fec4fc5d3190d2819ed150 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 9 Oct 2025 08:58:00 -0400 Subject: [PATCH 165/333] 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 166/333] 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 167/333] 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 168/333] 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 169/333] 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 170/333] 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 171/333] 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 172/333] 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 173/333] 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 174/333] 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 175/333] 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 176/333] 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 177/333] 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 178/333] 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 179/333] 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 180/333] 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 181/333] 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 182/333] 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 183/333] 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 184/333] 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 185/333] 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 186/333] 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 187/333] 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 188/333] 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 189/333] 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 190/333] 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 191/333] 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 192/333] 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 193/333] 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 194/333] 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 8a0bf224aa906b9b48bbf68cf901925b05601576 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 22:00:42 -0400 Subject: [PATCH 195/333] Fix merge issues --- lib/solargraph/pin/local_variable.rb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 63ca58483..6998b1d09 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -16,7 +16,7 @@ def presence_certain? # 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, exclude_return_type: nil, + def initialize presence: nil, presence_certain: false, exclude_return_type: nil, **splat super(**splat) @presence = presence @@ -161,16 +161,6 @@ def combine_mass_assignment(other) 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 - # 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] From 1cda7aca78d7d17636f0a5e5c59531c18eef5bd8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 22:28:04 -0400 Subject: [PATCH 196/333] Fix merge issues --- spec/parser/flow_sensitive_typing_spec.rb | 6 +++--- spec/source/chain/call_spec.rb | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index cac5523c0..dcc256956 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -552,7 +552,7 @@ def verify_repro(repr, throw_the_dice) 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') + expect(clip.infer.rooted_tags).to eq('::Integer, nil') end it 'uses variable in a simple if() to refine types' do @@ -595,6 +595,8 @@ def verify_repro(repr = nil) clip = api_map.clip_at('test.rb', [3, 8]) expect(clip.infer.rooted_tags).to eq('10, nil') + pending('TODO: regression?') + clip = api_map.clip_at('test.rb', [5, 10]) expect(clip.infer.rooted_tags).to eq('10') @@ -662,8 +664,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') diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 7dea01b76..e02f8f0bd 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -661,8 +661,6 @@ def bl end it 'sends proper gates in ProxyType' do - pending 'Proxytype improvements' - source = Solargraph::Source.load_string(%( module Foo module Bar From 41e7d49b14ee2a4bb9edb5cc71d1ac65b86e31cf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 22:37:51 -0400 Subject: [PATCH 197/333] Add @sg-ignore --- lib/solargraph/source/chain/or.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/source/chain/or.rb b/lib/solargraph/source/chain/or.rb index 6f4c9e23d..0f422c384 100644 --- a/lib/solargraph/source/chain/or.rb +++ b/lib/solargraph/source/chain/or.rb @@ -17,6 +17,8 @@ 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 545ca32c0ef58db19b77eba83e79d7d2f26688d8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 22:56:46 -0400 Subject: [PATCH 198/333] Fix @sg-ignore issues --- lib/solargraph/pin/base_variable.rb | 3 --- lib/solargraph/pin/local_variable.rb | 3 --- lib/solargraph/pin/parameter.rb | 1 - 3 files changed, 7 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 433351c29..b2c30f4c1 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 6998b1d09..3cf8a0e82 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -63,12 +63,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 7ef684934..77eb55e86 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 From 64c94b46a125eff124c6e1d509d16d81cfcd2b69 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 23:33:24 -0400 Subject: [PATCH 199/333] Fix merge issue --- spec/type_checker/levels/strong_spec.rb | 57 ------------------------- 1 file changed, 57 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index c3ca712ba..628c9d1b0 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -596,63 +596,6 @@ def objects_by_class klass end end - it 'resolves constants inside modules inside classes' do - checker = type_checker(%( - class Bar - module Foo - CONSTANT = 'hi' - end - end - - class Bar - include Foo - - # @return [String] - def baz - CONSTANT - end - end - )) - 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 dd1b0c25bf6e8f4df7d0b149649e15d4fa064bf7 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 24 Oct 2025 23:41:56 -0400 Subject: [PATCH 200/333] Adjust RuboCop todo --- .rubocop_todo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0df83216a..f29d03232 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -452,7 +452,7 @@ Metrics/AbcSize: # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 56 + Max: 57 # Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: From 05f1c1ec6a6ca6afe74b904260036ca6ea5189cf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 07:57:15 -0400 Subject: [PATCH 201/333] Fix merge issue --- lib/solargraph/source/chain/call.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 5fdbf1e30..c9430c0fb 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -52,7 +52,7 @@ def resolve api_map, name_pin, locals return yield_pins(api_map, name_pin) if word == 'yield' found = api_map.var_at_location(locals, word, name_pin, location) if head? - return inferred_pins(found, api_map, name_pin, locals) unless found.empty? + return inferred_pins(found, api_map, name_pin, locals) unless found.nil? 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) From 1d63a0d3db6cd29f64ee1820a2c584c55e500304 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 08:01:51 -0400 Subject: [PATCH 202/333] Fix merge issue --- lib/solargraph/type_checker.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 1e6903976..301bd393c 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -105,10 +105,10 @@ def load filename, level = :normal # @param level [Symbol] # @param api_map [Solargraph::ApiMap] # @return [self] - def load_string code, filename = nil, level = :normal, api_map: Solargraph::ApiMap.new + 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.loose_unions?) + api_map ||= Solargraph::ApiMap.new(loose_unions: rules.loose_unions?) api_map.map(source) new(filename, api_map: api_map, level: level, rules: rules) end From 36273f474735e50ce9c51a64455c697ae0bbfa8c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 08:02:57 -0400 Subject: [PATCH 203/333] Fix merge issue --- lib/solargraph/source/chain/call.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index c9430c0fb..41d02a214 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -52,7 +52,7 @@ def resolve api_map, name_pin, locals return yield_pins(api_map, name_pin) if word == 'yield' 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? + return inferred_pins([found], api_map, name_pin, locals) unless found.nil? 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) From a36cadcc3a05c6dc201b0438f83999acf250b211 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 08:16:15 -0400 Subject: [PATCH 204/333] Swap ivar expectations --- spec/type_checker/levels/strong_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 628c9d1b0..1ae981b12 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -651,12 +651,12 @@ def bar end )) + pending 'flow-sensitive typing improvements' + 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 From 1bae34bf0c0be4b0f03a2e781e28f58017740c0b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 10:02:07 -0400 Subject: [PATCH 205/333] Fix merge issue --- lib/solargraph/pin/local_variable.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index b87f20790..f1bfc3808 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -67,6 +67,7 @@ def combine_with(other, attrs={}) new_attrs = attrs.merge({ presence: combine_presence(other), presence_certain: combine_presence_certain(other), + exclude_return_type: combine_types(other, :exclude_return_type) }) super(other, new_attrs) end From 104f8abbf359e0a013a507291cf11753bb24207b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 10:19:56 -0400 Subject: [PATCH 206/333] Add probe method --- lib/solargraph/pin/local_variable.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index f1bfc3808..b52e96b1d 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -133,6 +133,19 @@ def combine_return_type(other) combine_types(other, :return_type) 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 + # @sg-ignore flow sensitive typing needs to handle ivars + return return_type.qualify(api_map, *gates) + end + + super + end + private attr_reader :exclude_return_type From 6151cb76a062a7c8d82ebdb6afba80bb0dd6b910 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 10:29:52 -0400 Subject: [PATCH 207/333] Fix merge issue --- lib/solargraph/pin/base_variable.rb | 3 ++- spec/parser/flow_sensitive_typing_spec.rb | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index b2c30f4c1..a3587241d 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -114,7 +114,8 @@ def return_types_from_node(parent_node, api_map) # @return [ComplexType] 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? + exclude_items = exclude_return_type&.items&.uniq + type_from_assignment = ComplexType.new(assignment_types.flat_map(&:items).uniq - (exclude_items || [])) unless assignment_types.empty? return type_from_assignment unless type_from_assignment.nil? # @todo should handle merging types from mass assignments as diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index dcc256956..9a49f041d 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -595,8 +595,6 @@ def verify_repro(repr = nil) clip = api_map.clip_at('test.rb', [3, 8]) expect(clip.infer.rooted_tags).to eq('10, nil') - pending('TODO: regression?') - clip = api_map.clip_at('test.rb', [5, 10]) expect(clip.infer.rooted_tags).to eq('10') From 9e2ba8a920f58ba5899cb83e857afc461f6bb448 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 10:34:12 -0400 Subject: [PATCH 208/333] Fix merge issue --- lib/solargraph/pin/base_variable.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index a3587241d..c91d01a77 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -110,6 +110,11 @@ def return_types_from_node(parent_node, api_map) types end + # @return [nil] + def exclude_return_type + nil + end + # @param api_map [ApiMap] # @return [ComplexType] def probe api_map From f72e4a00772f994f431b22621b9e0e89ba418577 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 10:51:58 -0400 Subject: [PATCH 209/333] Drop @sg-ignores --- lib/solargraph/api_map.rb | 1 - lib/solargraph/api_map/store.rb | 4 ---- lib/solargraph/doc_map.rb | 1 - lib/solargraph/pin/local_variable.rb | 1 - lib/solargraph/position.rb | 1 - lib/solargraph/rbs_map/conversions.rb | 1 - lib/solargraph/shell.rb | 2 -- lib/solargraph/source_map/mapper.rb | 6 ------ lib/solargraph/type_checker.rb | 1 - 9 files changed, 18 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 05c60f3d1..a428e647c 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -901,7 +901,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 ccd5e64d5..d52bbf71a 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -297,10 +297,6 @@ 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) diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 5dcf28552..05f2f1647 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -180,7 +180,6 @@ def load_serialized_gem_pins # @sg-ignore Need support for RBS duck interfaces like _ToHash # @type [Array] 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 diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index b52e96b1d..2203e5dd5 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -139,7 +139,6 @@ 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 - # @sg-ignore flow sensitive typing needs to handle ivars return return_type.qualify(api_map, *gates) end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index f0475bbe2..ec8605d18 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -57,7 +57,6 @@ def inspect # @return [Integer] def self.to_offset text, position return 0 if text.empty? - # @sg-ignore Unresolved call to + on Integer text.lines[0...position.line].sum(&:length) + position.character end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index f40824aa4..3e1874adb 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -353,7 +353,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 1a8514922..7ea659dc2 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -172,7 +172,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?) @@ -185,7 +184,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? diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 871f3ed5f..5fdcb9fe6 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -70,9 +70,6 @@ def closure_at(position) # @param comment [String] # @return [void] def process_comment source_position, comment_position, comment - # @sg-ignore Wrong argument type for String#=~: object - # expected String::_MatchAgainst, received - # Regexp return unless comment.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP cmnt = remove_inline_comment_hashes(comment) parse = Solargraph::Source.parse_docstring(cmnt) @@ -247,9 +244,6 @@ def remove_inline_comment_hashes comment # @return [void] def process_comment_directives - # @sg-ignore Wrong argument type for String#=~: object - # expected String::_MatchAgainst, received - # Regexp return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP code_lines = @code.lines @source.associated_comments.each do |line, comments| diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 301bd393c..a3cd0b575 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -505,7 +505,6 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi unless ptype.undefined? # @type [ComplexType] argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context) - # @sg-ignore Unresolved call to defined? 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 From 8e0bf1a5597af30c507e4badead1c0a9e8b7bc90 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 14:09:36 -0400 Subject: [PATCH 210/333] 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 211/333] 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 212/333] 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 213/333] 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 2263c950c2d3af547c2ca0b7856f14c3a7eb3ced Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 15:52:28 -0400 Subject: [PATCH 214/333] Fix type issues --- .../parser_gem/node_processors/or_node.rb | 23 +++++++++++++++++++ lib/solargraph/type_checker.rb | 2 +- lib/solargraph/yardoc.rb | 1 + 3 files changed, 25 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/or_node.rb b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb new file mode 100644 index 000000000..78f3a087a --- /dev/null +++ b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Solargraph + module Parser + module ParserGem + module NodeProcessors + class OrNode < Parser::NodeProcessor::Base + include ParserGem::NodeMethods + + def process + process_children + + # TODO: Write spec for below + + # FlowSensitiveTyping.new(locals, + # enclosing_breakable_pin, + # enclosing_compound_statement_pin).process_or(node) + end + end + end + end + end +end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index a3cd0b575..4fe36aeb5 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -103,7 +103,7 @@ def load filename, level = :normal # @param code [String] # @param filename [String, nil] # @param level [Symbol] - # @param api_map [Solargraph::ApiMap] + # @param api_map [Solargraph::ApiMap, nil] # @return [self] def load_string code, filename = nil, level = :normal, api_map: nil source = Solargraph::Source.load_string(code, filename) 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 47c472039f3f09c6c353ee716ef4c20e7bc7fdd4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 16:05:24 -0400 Subject: [PATCH 215/333] Fix solargraph-rspec issues --- 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 f51954aa00689264f007619d5014acc0bbe9a4c8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 16:10:59 -0400 Subject: [PATCH 216/333] 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 0834ab1011e09795a6d00b71432a522944d9de91 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 16:52:56 -0400 Subject: [PATCH 217/333] Add pending flag --- spec/type_checker/levels/strong_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 1ae981b12..1a51a9f62 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -628,7 +628,9 @@ def baz(foo) 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 + xit 'accepts ivar assignments and references with no intermediate calls as safe' do + pending 'support within flow-sensitive typing' + checker = type_checker(%( class Foo def initialize From 105a2c4ede03009034f0fc352e917d4a8e6843c2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 16:39:23 -0400 Subject: [PATCH 218/333] 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 d3c0185512a13d3d1f164522825ba2864e0dcdbd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 5 Oct 2025 16:39:23 -0400 Subject: [PATCH 219/333] 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 220/333] 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 a3ed92e4f7a61b66143a7d901003b73aa6c7f0be Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 17:34:03 -0400 Subject: [PATCH 221/333] Spec updates --- spec/type_checker/levels/alpha_spec.rb | 53 ++++++++++++++++++++++++ spec/type_checker/levels/strong_spec.rb | 55 ------------------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 824810038..1eabfba1e 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -71,5 +71,58 @@ def catalog expect(checker.problems.map(&:message)).to eq([]) end + + it 'accepts ivar assignments and references with no intermediate calls as 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 + 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 + 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/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 1a51a9f62..d66c74e83 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -627,60 +627,5 @@ def baz(foo) expect(checker.problems.map(&:location).map(&:range).map(&:start)).to be_empty end - - xit 'accepts ivar assignments and references with no intermediate calls as safe' do - pending 'support within flow-sensitive typing' - - 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 - )) - - pending 'flow-sensitive typing improvements' - - expect(checker.problems.map(&:message)).to be_empty - end - - it 'knows that ivar references with intermediate calls are not 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 - 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 From 4baadb502974a09245c104faee410edb53098bcc Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 17:40:16 -0400 Subject: [PATCH 222/333] Keep typechecking Ruby versions the same --- .github/workflows/plugins.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index cece27ae9..b2d458ef8 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' # keep same as typecheck.yml bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -43,7 +43,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec rails: @@ -54,7 +54,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: '3.4' # keep same as typecheck.yml bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -72,7 +72,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec rspec: @@ -83,7 +83,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: '3.4' # keep same as typecheck.yml bundler-cache: false - uses: awalsh128/cache-apt-pkgs-action@latest with: @@ -101,7 +101,7 @@ jobs: - name: Install gem types run: bundle exec rbs collection update - name: Ensure typechecking still works - run: bundle exec solargraph typecheck --level typed + run: bundle exec solargraph typecheck --level strong - name: Ensure specs still run run: bundle exec rake spec From da508cb3fc1a916ef694b35d610cfd4a1bd78479 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 20:27:55 -0400 Subject: [PATCH 223/333] 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 6ba610023ad0c4858a3130d29fd087d2905f93e2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 20:42:11 -0400 Subject: [PATCH 224/333] Annotation fixes --- lib/solargraph/api_map/store.rb | 4 ++++ lib/solargraph/language_server/host.rb | 1 + lib/solargraph/language_server/host/sources.rb | 1 + lib/solargraph/pin/closure.rb | 2 ++ lib/solargraph/pin/common.rb | 2 ++ lib/solargraph/pin/local_variable.rb | 1 + lib/solargraph/source_map.rb | 2 ++ lib/solargraph/source_map/mapper.rb | 4 ++-- lib/solargraph/workspace.rb | 1 + lib/solargraph/workspace/config.rb | 1 + lib/solargraph/workspace/require_paths.rb | 1 + 11 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index d52bbf71a..b43ae580e 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -240,9 +240,13 @@ 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? + # @param ref [String] refs.map(&:type).map(&:to_s).each do |ref| + # @sg-ignore We should understand reassignment of variable to new type next if ref.nil? || ref.empty? || visited.include?(ref) + # @sg-ignore We should understand reassignment of variable to new type ancestors << ref + # @sg-ignore We should understand reassignment of variable to new type queue << ref end end diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index 53da20175..8ac9f7b22 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -300,6 +300,7 @@ def prepare directory, name = nil end end + # @sg-ignore Need to validate config # @return [String] def command_path options['commandPath'] || 'solargraph' diff --git a/lib/solargraph/language_server/host/sources.rb b/lib/solargraph/language_server/host/sources.rb index da0c63b93..01aa47ad4 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 Need a better type for 'raise' # @return [Solargraph::Source] def find uri open_source_hash[uri] || raise(Solargraph::FileNotFoundError, "Host could not find #{uri}") diff --git a/lib/solargraph/pin/closure.rb b/lib/solargraph/pin/closure.rb index a7b37e01b..b0b42108f 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -44,6 +44,8 @@ def context end end + # @sg-ignore Solargraph::Pin::Closure#binder return type could not be inferred + # @return [Solargraph::ComplexType] def binder @binder || context end diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index 062099ee4..8129490e5 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -40,6 +40,8 @@ def namespace context.namespace.to_s end + # @sg-ignore Solargraph::Pin::Common#binder return type could + # not be inferred # @return [ComplexType] def binder @binder || context diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 2203e5dd5..0581aefe2 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -147,6 +147,7 @@ def probe api_map private + # @return [ComplexType, nil] attr_reader :exclude_return_type # @param tag1 [String] diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 895e6d6b8..f36bb5624 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -34,6 +34,8 @@ def locals # @param source [Source] def initialize source @source = source + # @type [Array, nil] + @convention_pins = nil conventions_environ.merge Convention.for_local(self) unless filename.nil? # FIXME: unmemoizing the document_symbols in case it was called and memoized from any of conventions above diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 5fdcb9fe6..0280ace8d 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -17,7 +17,7 @@ class Mapper # Generate the data. # # @param source [Source] - # @return [Array] + # @return [Array(Array, Array)] def map source @source = source @filename = source.filename @@ -46,7 +46,7 @@ def unmap filename, code class << self # @param source [Source] - # @return [Array] + # @return [Array(Array, Array)] def map source return new.unmap(source.filename, source.code) unless source.parsed? new.map source diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 07cf26f09..691d9b78d 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -137,6 +137,7 @@ def synchronize! updater source_hash[updater.filename] = source_hash[updater.filename].synchronize(updater) end + # @sg-ignore Need to validate config # @return [String] def command_path server['commandPath'] || 'solargraph' diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index 3e30e5d74..32018a587 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -78,6 +78,7 @@ def required # An array of load paths for required paths. # + # @sg-ignore Need to validate config # @return [Array] def require_paths raw_data['require_paths'] || [] diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index c8eea161b..10dce4053 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 Should handle redefinition of types in simple contexts Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end From e15c04d2a013be780059e485113033bd5a2d3f23 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 21:29:44 -0400 Subject: [PATCH 225/333] Fix issue with &. --- lib/solargraph/complex_type.rb | 4 +++- lib/solargraph/source/chain/call.rb | 10 +++++++++- spec/type_checker/levels/alpha_spec.rb | 13 +++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index e6e504bf3..3d1598c44 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -311,7 +311,9 @@ def nullable? # @return [ComplexType] def without_nil - ComplexType.new(@items.reject(&:nil_type?)) + new_items = @items.reject(&:nil_type?) + return ComplexType::UNDEFINED if new_items.empty? + ComplexType.new(new_items) end # @return [Array] diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index fc00450c0..8cdefbbec 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -53,7 +53,15 @@ def resolve api_map, name_pin, locals 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? - pin_groups = name_pin.binder.each_unique_type.map do |context| + binder = name_pin.binder + # this is a q_call - i.e., foo&.bar - assume result of call + # will be nil or result as if binder were not nil - + # chain.rb#maybe_nil will add the nil type later, we just + # need to worry about the not-nil case + + # @sg-ignore Need to handle duck-typed method calls on union types + binder = binder.without_nil if nullable? + pin_groups = 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 diff --git a/spec/type_checker/levels/alpha_spec.rb b/spec/type_checker/levels/alpha_spec.rb index 1eabfba1e..cdba7bc72 100644 --- a/spec/type_checker/levels/alpha_spec.rb +++ b/spec/type_checker/levels/alpha_spec.rb @@ -124,5 +124,18 @@ def bar expect(checker.problems.map(&:message)).to eq(["Foo#bar return type could not be inferred", "Unresolved call to round"]) end + + it 'understands &. in return position' do + checker = type_checker(%( + class Baz + # @param bar [String, nil] + # @return [String] + def foo bar + bar&.upcase || 'undefined' + end + end + )) + expect(checker.problems.map(&:message)).to be_empty + end end end From c03d238cd401fc50e554a3e68dd70cbd671f9322 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 21:40:33 -0400 Subject: [PATCH 226/333] Fix merge --- spec/parser/flow_sensitive_typing_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index b91237d2c..df6d63b65 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -197,6 +197,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] From cf9a72a484ccaacfcfd9f816ad2442e1afffc935 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 25 Oct 2025 21:46:33 -0400 Subject: [PATCH 227/333] Drop accidental file add --- .../parser_gem/node_processors/or_node.rb | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 lib/solargraph/parser/parser_gem/node_processors/or_node.rb diff --git a/lib/solargraph/parser/parser_gem/node_processors/or_node.rb b/lib/solargraph/parser/parser_gem/node_processors/or_node.rb deleted file mode 100644 index 78f3a087a..000000000 --- a/lib/solargraph/parser/parser_gem/node_processors/or_node.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - module Parser - module ParserGem - module NodeProcessors - class OrNode < Parser::NodeProcessor::Base - include ParserGem::NodeMethods - - def process - process_children - - # TODO: Write spec for below - - # FlowSensitiveTyping.new(locals, - # enclosing_breakable_pin, - # enclosing_compound_statement_pin).process_or(node) - end - end - end - end - end -end From 6ff7332f8abae849ee874138fadb4516ccb74090 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 26 Oct 2025 10:21:11 -0400 Subject: [PATCH 228/333] Remove @sg-ignore --- 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 08816d8ab6e37fec50b4b0702e98642a2d3b9b24 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 26 Oct 2025 10:27:39 -0400 Subject: [PATCH 229/333] 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 230/333] 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 231/333] 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 232/333] 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 233/333] 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 234/333] 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 235/333] 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 236/333] 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 237/333] 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 238/333] 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 9bc22d4f1e6e09e759a8bec9b135472b5712e50e Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 09:36:30 -0400 Subject: [PATCH 239/333] Drop @sg-ignore --- lib/solargraph/type_checker.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 952dca7c0..29f1ebc73 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -228,7 +228,6 @@ 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? From ba66987a91ab7ac77c973c5a3dd424fcc8020e45 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 12:37:48 -0400 Subject: [PATCH 240/333] 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 241/333] 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 077ddd94ee42c943a1ec713de8f270879918efde Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 13:31:56 -0400 Subject: [PATCH 242/333] Fix merge --- lib/solargraph/api_map/store.rb | 3 --- lib/solargraph/parser/flow_sensitive_typing.rb | 3 ++- lib/solargraph/parser/parser_gem/node_methods.rb | 2 ++ lib/solargraph/workspace/require_paths.rb | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index b43ae580e..480b77525 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -242,11 +242,8 @@ def get_ancestors(fqns) next if refs.nil? # @param ref [String] refs.map(&:type).map(&:to_s).each do |ref| - # @sg-ignore We should understand reassignment of variable to new type next if ref.nil? || ref.empty? || visited.include?(ref) - # @sg-ignore We should understand reassignment of variable to new type ancestors << ref - # @sg-ignore We should understand reassignment of variable to new type queue << ref end end diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 94b525635..93a7649fe 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -210,7 +210,7 @@ class << self # @return [void] def add_downcast_local(pin, presence:, downcast_type:, downcast_not_type:) # @todo Create pin#update method - new_pin = pin.class.new( + new_pin = Pin::LocalVariable.new( location: pin.location, closure: pin.closure, name: pin.name, @@ -223,6 +223,7 @@ def add_downcast_local(pin, presence:, downcast_type:, downcast_not_type:) source: :flow_sensitive_typing ) new_pin.reset_generated! + new_pin = pin.combine_with(new_pin) locals.push(new_pin) end diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 59d705120..b92dfe575 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,6 +119,8 @@ 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/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index 10dce4053..c8eea161b 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -83,7 +83,6 @@ 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 Should handle redefinition of types in simple contexts Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end From bfe7f4e73f627433cbeb9851072490cdb046e314 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 1 Nov 2025 16:54:49 -0400 Subject: [PATCH 243/333] 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 244/333] 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 245/333] 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 246/333] 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 247/333] 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 248/333] 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 249/333] 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 250/333] 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 e6dcf0970cc02d64063663b4859e2889a381a717 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 09:56:04 -0500 Subject: [PATCH 251/333] Mark item fixed --- spec/pin/base_variable_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 8c07e9361..2a49ac9f1 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -46,8 +46,6 @@ def bar end it "understands proc kwarg parameters aren't affected by @type" do - pending 'better kwarg handling in type checker' - code = %( # @return [Proc] def foo From c5df6965b99536d7dc0efde20e19ada24bcf34ed Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 3 Nov 2025 10:07:01 -0500 Subject: [PATCH 252/333] 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 253/333] 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 254/333] 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 255/333] 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 256/333] 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 257/333] 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 258/333] 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 259/333] 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 260/333] 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 261/333] 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 262/333] 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 263/333] 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 264/333] 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 265/333] 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 266/333] 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 267/333] 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 268/333] 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 269/333] 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 270/333] 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 271/333] 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 272/333] 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 273/333] 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 274/333] 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 275/333] 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 276/333] 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 277/333] 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 278/333] 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 279/333] 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 280/333] 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 281/333] 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 282/333] 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 283/333] 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 284/333] 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 285/333] 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 286/333] 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 287/333] 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 288/333] 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 289/333] 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 290/333] 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 fe06cf3e2fe8e22e3efefe46fe85e9adaa0913f5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 19:08:24 -0500 Subject: [PATCH 291/333] 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 9b6aa30353fb22baeee09ebaa573a96021d68db1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 19:13:25 -0500 Subject: [PATCH 292/333] Pull in solargraph-rspec fixes from another branch --- .github/workflows/plugins.yml | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 1c633fda0..fa704e2ae 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -116,19 +116,39 @@ 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: - ruby-version: 3.4 + ruby-version: '3.1' + rubygems: latest bundler-cache: false - name: Install gems run: | + set -x + 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 + # @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 + # 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 @@ -136,12 +156,13 @@ 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 solargraph gems $rspec_gems + bundle exec appraisal rbs collection update + 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: | 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 19eae82aa99710ddf1882abd841406b270b3ec24 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 19:23:51 -0500 Subject: [PATCH 293/333] Drop @sg-ignores --- lib/solargraph/pin/base_variable.rb | 1 - lib/solargraph/shell.rb | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index da1f15cb2..fbf22cbaa 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -28,7 +28,6 @@ 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 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 482671b6a1ff201c292d47b9bab1b15533037bb8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 19:26:16 -0500 Subject: [PATCH 294/333] Add @sg-ignore --- lib/solargraph/parser/parser_gem/node_methods.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 02f790c00..b77c4cd47 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,6 +119,7 @@ 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] From b1d0b21818319684d51cc9e303f24448de565f43 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 20:06:28 -0500 Subject: [PATCH 295/333] Drop @sg-ignore --- 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 973b512b121a23bd53fc77c3d2c6caa718c511e2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 20:21:11 -0500 Subject: [PATCH 296/333] 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 fa704e2ae..c7ad72cb4 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 390d68a1122525904df6f8b393d359874bb33129 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 21:42:34 -0500 Subject: [PATCH 297/333] Remove @sg-ignores --- lib/solargraph/parser/parser_gem/node_methods.rb | 2 -- lib/solargraph/pin/closure.rb | 1 - lib/solargraph/pin/common.rb | 1 - 3 files changed, 4 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index b92dfe575..59d705120 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -119,8 +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/closure.rb b/lib/solargraph/pin/closure.rb index bd372cc45..a644f3791 100644 --- a/lib/solargraph/pin/closure.rb +++ b/lib/solargraph/pin/closure.rb @@ -44,7 +44,6 @@ def context end end - # @sg-ignore Solargraph::Pin::Closure#binder return type could not be inferred # @return [Solargraph::ComplexType] def binder @binder || context diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index a7d4beb0d..646e79149 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 From f9114dc87aa4dd92a556f90930921933a0e97c36 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 21:44:59 -0500 Subject: [PATCH 298/333] Add @sg-ignores --- lib/solargraph/api_map/store.rb | 3 +++ lib/solargraph/workspace/require_paths.rb | 1 + 2 files changed, 4 insertions(+) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 480b77525..b43ae580e 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -242,8 +242,11 @@ def get_ancestors(fqns) next if refs.nil? # @param ref [String] refs.map(&:type).map(&:to_s).each do |ref| + # @sg-ignore We should understand reassignment of variable to new type next if ref.nil? || ref.empty? || visited.include?(ref) + # @sg-ignore We should understand reassignment of variable to new type ancestors << ref + # @sg-ignore We should understand reassignment of variable to new type queue << ref end end diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index c8eea161b..10dce4053 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 Should handle redefinition of types in simple contexts Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}" [] end From 680638dacf08b24ab56ec9fbb2d9a5c7a748509d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 22:33:17 -0500 Subject: [PATCH 299/333] Adjust @sg-ignores --- lib/solargraph/source/chain/call.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 7b515be2a..287d59f37 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -58,8 +58,6 @@ def resolve api_map, name_pin, locals # will be nil or result as if binder were not nil - # chain.rb#maybe_nil will add the nil type later, we just # need to worry about the not-nil case - - # @sg-ignore Need to handle duck-typed method calls on union types binder = binder.without_nil if nullable? pin_groups = binder.each_unique_type.map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag @@ -256,7 +254,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 + # @sg-ignore Need to support this in flow-sensitive typing method_pin = method_pin.closure return if method_pin.nil? end From 6f2e8bb9a45740437eb4bc1052b41be4aef2d56a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 22:38:31 -0500 Subject: [PATCH 300/333] Restore @sg-ignores --- lib/solargraph/source/chain/call.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 287d59f37..9adea1a1e 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -58,6 +58,8 @@ def resolve api_map, name_pin, locals # will be nil or result as if binder were not nil - # chain.rb#maybe_nil will add the nil type later, we just # need to worry about the not-nil case + + # @sg-ignore Need to handle duck-typed method calls on union types binder = binder.without_nil if nullable? pin_groups = binder.each_unique_type.map do |context| ns_tag = context.namespace == '' ? '' : context.namespace_type.tag From 4f94ef8c2a25f5fc80a7f38e69915022935aca24 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 22:48:36 -0500 Subject: [PATCH 301/333] 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 c7028dce7b44fa67149cbc1ec8c6521b57e31854 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 23:21:05 -0500 Subject: [PATCH 302/333] Mark spec as pending --- spec/pin/base_variable_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index 2a49ac9f1..c87c31884 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -46,6 +46,8 @@ def bar end it "understands proc kwarg parameters aren't affected by @type" do + pending "understanding restarg in block param in Block#typify_parameters" + code = %( # @return [Proc] def foo From 0d854cda9699d07273ca8fba62b28568f53cb7f5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 23:29:40 -0500 Subject: [PATCH 303/333] Mark spec as pending --- spec/pin/base_variable_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pin/base_variable_spec.rb b/spec/pin/base_variable_spec.rb index c87c31884..03e6b1a11 100644 --- a/spec/pin/base_variable_spec.rb +++ b/spec/pin/base_variable_spec.rb @@ -45,7 +45,7 @@ def bar expect(type.simplify_literals.to_rbs).to eq('(::Integer | ::NilClass)') end - it "understands proc kwarg parameters aren't affected by @type" do + xit "understands proc kwarg parameters aren't affected by @type" do pending "understanding restarg in block param in Block#typify_parameters" code = %( From f8ded08e41ae362757f08e1ede6fd0d1e56cc1e9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 15 Nov 2025 23:44:01 -0500 Subject: [PATCH 304/333] Add @sg-ignores --- lib/solargraph/library.rb | 2 ++ lib/solargraph/pin/delegated_method.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index 5c7851201..b0afaad4e 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -303,7 +303,9 @@ def locate_ref location return nil if pin.nil? # @param full [String] return_if_match = proc do |full| + # @sg-ignore Need to handle restarg parameters in block parameters if source_map_hash.key?(full) + # @sg-ignore Need to handle restarg parameters in block parameters return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) end end diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index bcf5b5912..530804747 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -51,6 +51,7 @@ def type_location %i[typify realize infer probe].each do |method| # @param api_map [ApiMap] define_method(method) do |api_map| + # @sg-ignore Need to handle restarg parameters in block parameters resolve_method(api_map) # @sg-ignore Need to set context correctly in define_method blocks @resolved_method ? @resolved_method.send(method, api_map) : super(api_map) From ec29b25fd0f833ae778b750b9d9517c2726e9ba1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 00:13:45 -0500 Subject: [PATCH 305/333] 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 4d0366b73243fc94211d39a6ba38b9db1907fb98 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 10:58:54 -0500 Subject: [PATCH 306/333] Use latest rubygems --- .github/workflows/plugins.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index b0ab6ecec..99bbc386a 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -180,6 +180,8 @@ jobs: # solargraph-rails supports Ruby 3.0+ ruby-version: '3.0' bundler-cache: false + # https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17 + rubygems: latest bundler: latest env: MATRIX_RAILS_VERSION: "7.0" From 963c13538f2e05650610d345a5df7dbd8d2a07e5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 16 Nov 2025 11:30:28 -0500 Subject: [PATCH 307/333] 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 308/333] 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 309/333] 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 310/333] 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 311/333] 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 312/333] 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 313/333] 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 314/333] 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 315/333] 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 316/333] 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 317/333] 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 318/333] 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 319/333] 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 320/333] 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 9ec6433b632107bb6f71925033c72b659e608a66 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 27 Nov 2025 08:14:48 -0500 Subject: [PATCH 321/333] Merge fix --- lib/solargraph/pin/base_variable.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index 7c448aad4..61adf1019 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -90,10 +90,6 @@ def combine_with(other, attrs={}) super(other, new_attrs) 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] @@ -116,7 +112,7 @@ def combine_assignments(other) end def inner_desc - super + ", presence=#{presence.inspect}, assignments=#{assignments}" + super + ", intersection_return_type=#{intersection_return_type&.rooted_tags.inspect}, exclude_return_type=#{exclude_return_type&.rooted_tags.inspect}, presence=#{presence.inspect}, assignments=#{assignments}" end def completion_item_kind From 8e2a094019c753e66757a6d67e73839ff9dd19c8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 27 Nov 2025 08:19:01 -0500 Subject: [PATCH 322/333] Fix merge --- .github/workflows/plugins.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 3cb2b3106..576aa5e0e 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -179,8 +179,6 @@ jobs: # https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17 rubygems: latest 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 6e558b724b75401969fd656fc35f8ffa4c4d297d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 13:12:03 -0500 Subject: [PATCH 323/333] Avoid ignoring valid pins in typechecking An overly-conservative check in type_checker.rb was suppressing later warnings on method parameters with no default values --- lib/solargraph/type_checker.rb | 7 ++++--- spec/type_checker/levels/strong_spec.rb | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 4600767b5..262d65137 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -195,7 +195,7 @@ def variable_type_tag_problems if pin.return_type.defined? declared = pin.typify(api_map) next if declared.duck_type? - if declared.defined? + if declared.defined? && pin.assignment if rules.validate_tags? inferred = pin.probe(api_map) if inferred.undefined? @@ -216,7 +216,7 @@ def variable_type_tag_problems elsif !pin.is_a?(Pin::Parameter) && !resolved_constant?(pin) result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}", pin: pin) end - else + elsif pin.assignment inferred = pin.probe(api_map) if inferred.undefined? && declared_externally?(pin) ignored_pins.push pin @@ -604,7 +604,8 @@ def external? pin # @param pin [Pin::BaseVariable] def declared_externally? pin - return true if pin.assignment.nil? + raise "No assignment found" if pin.assignment.nil? + chain = Solargraph::Parser.chain(pin.assignment, filename) rng = Solargraph::Range.from_node(pin.assignment) closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) diff --git a/spec/type_checker/levels/strong_spec.rb b/spec/type_checker/levels/strong_spec.rb index 970435dc3..5c7544f17 100644 --- a/spec/type_checker/levels/strong_spec.rb +++ b/spec/type_checker/levels/strong_spec.rb @@ -4,6 +4,21 @@ def type_checker(code) Solargraph::TypeChecker.load_string(code, 'test.rb', :strong) end + it 'provides nil checking on calls from parameters without assignments' do + pending('https://github.com/castwide/solargraph/pull/1127') + + checker = type_checker(%( + # @param baz [String, nil] + # + # @return [String] + def quux(baz) + baz.upcase # ERROR: Unresolved call to upcase on String, nil + end + )) + expect(checker.problems.map(&:message)).to eq(['#quux return type could not be inferred', + 'Unresolved call to upcase on String, nil']) + end + it 'does not complain on array dereference' do checker = type_checker(%( # @param idx [Integer, nil] an index From 890d638cb18b9f09c2f34893eb121b1d52062a33 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 13:21:17 -0500 Subject: [PATCH 324/333] Fix newly found type issue --- lib/solargraph/shell.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 046a74296..688ec90ff 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -273,6 +273,7 @@ def pin path else pins = api_map.get_path_pins path end + # @type [Hash{Symbol => Pin::Base}] references = {} pin = pins.first case pin From 40f547f0fb0fbceadb567eb447a9f06cfb801d7b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 15:55:08 -0500 Subject: [PATCH 325/333] Annotations needed after https://github.com/castwide/solargraph/pull/1130 --- lib/solargraph/api_map.rb | 1 + lib/solargraph/api_map/constants.rb | 12 +++---- lib/solargraph/api_map/index.rb | 34 ++++++++++++++++--- lib/solargraph/api_map/source_to_yard.rb | 3 ++ lib/solargraph/api_map/store.rb | 11 ++++-- lib/solargraph/complex_type.rb | 1 + lib/solargraph/complex_type/type_methods.rb | 1 + lib/solargraph/complex_type/unique_type.rb | 6 ++++ lib/solargraph/diagnostics/rubocop_helpers.rb | 5 ++- lib/solargraph/doc_map.rb | 3 ++ lib/solargraph/equality.rb | 1 + lib/solargraph/gem_pins.rb | 3 ++ lib/solargraph/language_server/host.rb | 1 + .../message/text_document/formatting.rb | 2 ++ lib/solargraph/location.rb | 1 + .../parser_gem/node_processors/and_node.rb | 1 + .../parser_gem/node_processors/send_node.rb | 2 ++ lib/solargraph/pin/base.rb | 2 ++ lib/solargraph/pin/base_variable.rb | 1 + lib/solargraph/pin/block.rb | 1 + lib/solargraph/pin/callable.rb | 2 ++ lib/solargraph/pin/method.rb | 18 +++++++++- lib/solargraph/pin/parameter.rb | 4 +++ lib/solargraph/pin/search.rb | 3 ++ lib/solargraph/position.rb | 1 + lib/solargraph/range.rb | 4 +++ lib/solargraph/rbs_map/conversions.rb | 13 +++++++ lib/solargraph/shell.rb | 2 ++ lib/solargraph/source/chain.rb | 2 ++ lib/solargraph/source/chain/call.rb | 1 + lib/solargraph/source/chain/if.rb | 2 +- lib/solargraph/source/chain/link.rb | 2 +- lib/solargraph/source/chain/or.rb | 2 +- lib/solargraph/type_checker.rb | 2 ++ lib/solargraph/yard_map/mapper/to_method.rb | 3 +- lib/solargraph/yard_map/to_method.rb | 3 +- solargraph.gemspec | 1 + 37 files changed, 136 insertions(+), 21 deletions(-) diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index c4fd49134..936f7406d 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -39,6 +39,7 @@ def initialize pins: [] # @param other [Object] def eql?(other) self.class == other.class && + # @sg-ignore Flow sensitive typing needs to handle self.class == other.class equality_fields == other.equality_fields end diff --git a/lib/solargraph/api_map/constants.rb b/lib/solargraph/api_map/constants.rb index 0df8d83ce..f643e2ad2 100644 --- a/lib/solargraph/api_map/constants.rb +++ b/lib/solargraph/api_map/constants.rb @@ -45,7 +45,7 @@ def dereference pin # Collect a list of all constants defined in the specified gates. # # @param gates [Array, String>] - # @return [Array] + # @return [Array] def collect(*gates) flat = gates.flatten cached_collect[flat] || collect_and_cache(flat) @@ -155,7 +155,7 @@ def simple_resolve name, gate, internal end # @param gates [Array] - # @return [Array] + # @return [Array] def collect_and_cache gates skip = Set.new cached_collect[gates] = gates.flat_map do |gate| @@ -168,7 +168,7 @@ def cached_resolve @cached_resolve ||= {} end - # @return [Hash{Array => Array}] + # @return [Hash{Array => Array}] def cached_collect @cached_collect ||= {} end @@ -213,7 +213,7 @@ def inner_qualify name, root, skip return fqns if store.namespace_exists?(fqns) incs = store.get_includes(roots.join('::')) incs.each do |inc| - foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip) + foundinc = inner_qualify(name, inc.type.to_s, skip) possibles.push foundinc unless foundinc.nil? end roots.pop @@ -221,7 +221,7 @@ def inner_qualify name, root, skip if possibles.empty? incs = store.get_includes('') incs.each do |inc| - foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip) + foundinc = inner_qualify(name, inc.type.to_s, skip) possibles.push foundinc unless foundinc.nil? end end @@ -233,7 +233,7 @@ def inner_qualify name, root, skip # @param fqns [String] # @param visibility [Array] # @param skip [Set] - # @return [Array] + # @return [Array] def inner_get_constants fqns, visibility, skip return [] if fqns.nil? || skip.include?(fqns) skip.add fqns diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index cb61a28eb..944f02c79 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -17,16 +17,22 @@ def pins # @return [Hash{String => Array}] def namespace_hash + # @param h [String] + # @param k [Array] @namespace_hash ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def pin_class_hash + # @param h [String] + # @param k [Array] @pin_class_hash ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def path_pin_hash + # @param h [String] + # @param k [Array] @path_pin_hash ||= Hash.new { |h, k| h[k] = [] } end @@ -42,26 +48,36 @@ def pins_by_class klass # @return [Hash{String => Array}] def include_references + # @param h [String] + # @param k [Array] @include_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def include_reference_pins + # @param h [String] + # @param k [Array] @include_reference_pins ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def extend_references + # @param h [String] + # @param k [Array] @extend_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def prepend_references + # @param h [String] + # @param k [Array] @prepend_references ||= Hash.new { |h, k| h[k] = [] } end # @return [Hash{String => Array}] def superclass_references + # @param h [String] + # @param k [Array] @superclass_references ||= Hash.new { |h, k| h[k] = [] } end @@ -99,12 +115,18 @@ def catalog new_pins @pin_select_cache = {} pins.concat new_pins set = new_pins.to_set + # @param k [String] + # @param v [Set] set.classify(&:class) - .map { |k, v| pin_class_hash[k].concat v.to_a } + .map { |k, v| pin_class_hash[k].concat v.to_a } + # @param k [String] + # @param v [Set] set.classify(&:namespace) - .map { |k, v| namespace_hash[k].concat v.to_a } + .map { |k, v| namespace_hash[k].concat v.to_a } + # @param k [String] + # @param v [Set] set.classify(&:path) - .map { |k, v| path_pin_hash[k].concat v.to_a } + .map { |k, v| path_pin_hash[k].concat v.to_a } @namespaces = path_pin_hash.keys.compact.to_set map_references Pin::Reference::Include, include_references map_references Pin::Reference::Prepend, prepend_references @@ -115,11 +137,12 @@ def catalog new_pins end # @generic T - # @param klass [Class] - # @param hash [Hash{String => T}] + # @param klass [Class>] + # @param hash [Hash{String => generic}] # # @return [void] def map_references klass, hash + # @param pin [generic] pins_by_class(klass).each do |pin| hash[pin.namespace].push pin end @@ -127,6 +150,7 @@ def map_references klass, hash # @return [void] def map_overrides + # @param ovr [Pin::Reference::Override] pins_by_class(Pin::Reference::Override).each do |ovr| logger.debug { "ApiMap::Index#map_overrides: Looking at override #{ovr} for #{ovr.name}" } pins = path_pin_hash[ovr.name] diff --git a/lib/solargraph/api_map/source_to_yard.rb b/lib/solargraph/api_map/source_to_yard.rb index ccbed3eb6..b45754d80 100644 --- a/lib/solargraph/api_map/source_to_yard.rb +++ b/lib/solargraph/api_map/source_to_yard.rb @@ -32,11 +32,13 @@ def rake_yard store next end if pin.type == :class + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::ClassObject.new(root_code_object, pin.path) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) } else + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::ModuleObject.new(root_code_object, pin.path) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?) @@ -65,6 +67,7 @@ def rake_yard store next end + # @param obj [YARD::CodeObjects::RootObject] code_object_map[pin.path] ||= YARD::CodeObjects::MethodObject.new(code_object_at(pin.namespace, YARD::CodeObjects::NamespaceObject), pin.name, pin.scope) { |obj| next if pin.location.nil? || pin.location.filename.nil? obj.add_file pin.location.filename, pin.location.range.start.line diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index c41e19c09..32fa8bfd4 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -17,7 +17,7 @@ def pins index.pins end - # @param pinsets [Array>] + # @param pinsets [Array>] # - pinsets[0] = core Ruby pins # - pinsets[1] = documentation/gem pins # - pinsets[2] = convention pins @@ -60,6 +60,7 @@ def inspect # @return [Enumerable] def get_constants fqns, visibility = [:public] namespace_children(fqns).select { |pin| + # @sg-ignore flow-sensitive typing not smart enough to handle this case !pin.name.empty? && (pin.is_a?(Pin::Namespace) || pin.is_a?(Pin::Constant)) && visibility.include?(pin.visibility) } end @@ -70,6 +71,7 @@ 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) @@ -258,7 +260,7 @@ def get_ancestors(fqns) # @param fqns [String] # - # @return [Array] + # @return [Array] def get_ancestor_references(fqns) (get_prepends(fqns) + get_includes(fqns) + [get_superclass(fqns)]).compact end @@ -275,7 +277,7 @@ def index @indexes.last end - # @param pinsets [Array>] + # @param pinsets [Array>] # # @return [void] def catalog pinsets @@ -296,6 +298,9 @@ def catalog pinsets # @return [Hash{::Array(String, String) => ::Array}] def fqns_pins_map + # @param h [Hash{::Array(String, String) => ::Array}] + # @param base [String] + # @param name [String] @fqns_pins_map ||= Hash.new do |h, (base, name)| value = namespace_children(base).select { |pin| pin.name == name && pin.is_a?(Pin::Namespace) } h[[base, name]] = value diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 669a66900..555d60947 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -324,6 +324,7 @@ def parse *strings, partial: false paren_stack = 0 base = String.new subtype_string = String.new + # @param char [String] type_string&.each_char do |char| if char == '=' #raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0 diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb index d8d4fc7d7..ead1559af 100644 --- a/lib/solargraph/complex_type/type_methods.rb +++ b/lib/solargraph/complex_type/type_methods.rb @@ -190,6 +190,7 @@ def scope # @param other [Object] def == other return false unless self.class == other.class + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 tag == other.tag end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 05a585dcf..e490bff35 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -138,11 +138,17 @@ def determine_non_literal_name def eql?(other) self.class == other.class && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @name == other.name && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @key_types == other.key_types && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @subtypes == other.subtypes && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @rooted == other.rooted? && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @all_params == other.all_params && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 @parameters_type == other.parameters_type end diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index f6f4c82c8..fc458956e 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -20,9 +20,11 @@ def require_rubocop(version = nil) gem_lib_path = File.join(gem_path, 'lib') $LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path) rescue Gem::MissingSpecVersionError => e + # @type [Array] + specs = e.specs raise InvalidRubocopVersionError, "could not find '#{e.name}' (#{e.requirement}) - "\ - "did find: [#{e.specs.map { |s| s.version.version }.join(', ')}]" + "did find: [#{specs.map { |s| s.version.version }.join(', ')}]" end require 'rubocop' end @@ -36,6 +38,7 @@ def generate_options filename, code args = ['-f', 'j', '--force-exclusion', filename] base_options = RuboCop::Options.new options, paths = base_options.parse(args) + # @sg-ignore options[:stdin] = code [options, paths] end diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 05f2f1647..96fa13acd 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -358,13 +358,16 @@ def change_gemspec_version gemspec, version # @return [Array] def fetch_dependencies gemspec # @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 end diff --git a/lib/solargraph/equality.rb b/lib/solargraph/equality.rb index 0667efacd..f8c50ff31 100644 --- a/lib/solargraph/equality.rb +++ b/lib/solargraph/equality.rb @@ -12,6 +12,7 @@ module Equality # @return [Boolean] def eql?(other) self.class.eql?(other.class) && + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 equality_fields.eql?(other.equality_fields) end diff --git a/lib/solargraph/gem_pins.rb b/lib/solargraph/gem_pins.rb index a193a8a39..1c4330389 100644 --- a/lib/solargraph/gem_pins.rb +++ b/lib/solargraph/gem_pins.rb @@ -27,6 +27,8 @@ def self.combine_method_pins_by_path(pins) def self.combine_method_pins(*pins) # @type [Pin::Method, nil] combined_pin = nil + # @param memo [Pin::Method, nil] + # @param pin [Pin::Method] out = pins.reduce(combined_pin) do |memo, pin| next pin if memo.nil? if memo == pin && memo.source != :combined @@ -63,6 +65,7 @@ def self.combine(yard_pins, rbs_pins) next yard_pin unless rbs_pin && yard_pin.class == 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/language_server/host.rb b/lib/solargraph/language_server/host.rb index 53da20175..b228bdba6 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -504,6 +504,7 @@ def locate_pins params name: 'new', scope: :class, location: pin.location, + # @sg-ignore Unresolved call to parameters on Solargraph::Pin::Base parameters: pin.parameters, return_type: ComplexType.try_parse(params['data']['path']), comments: pin.comments, diff --git a/lib/solargraph/language_server/message/text_document/formatting.rb b/lib/solargraph/language_server/message/text_document/formatting.rb index 821de7ffc..d67a0b414 100644 --- a/lib/solargraph/language_server/message/text_document/formatting.rb +++ b/lib/solargraph/language_server/message/text_document/formatting.rb @@ -18,6 +18,7 @@ def process require_rubocop(config['version']) options, paths = ::RuboCop::Options.new.parse(args) + # @sg-ignore Unresolved call to []= options[:stdin] = original # Ensure only one instance of RuboCop::Runner is running at @@ -28,6 +29,7 @@ def process ::RuboCop::Runner.new(options, ::RuboCop::ConfigStore.new).run(paths) end end + # @sg-ignore Unresolved call to []= result = options[:stdin] log_corrections(corrections) diff --git a/lib/solargraph/location.rb b/lib/solargraph/location.rb index 713b4fef1..df92668bf 100644 --- a/lib/solargraph/location.rb +++ b/lib/solargraph/location.rb @@ -71,6 +71,7 @@ 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/parser/parser_gem/node_processors/and_node.rb b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb index d3485af7c..a761ae38c 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/and_node.rb @@ -11,6 +11,7 @@ def process process_children position = get_node_start_position(node) + # @sg-ignore https://github.com/castwide/solargraph/pull/1114 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 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 645baf00f..e63a23abc 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -231,6 +231,7 @@ 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 ) @@ -239,6 +240,7 @@ 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/base.rb b/lib/solargraph/pin/base.rb index 511c7deb7..57d083453 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -259,6 +259,7 @@ def rbs_location? def assert_same_macros(other) return unless self.source == :yardoc && other.source == :yardoc assert_same_count(other, :macros) + # @param [YARD::Tags::MacroDirective] assert_same_array_content(other, :macros) { |macro| macro.tag.name } end @@ -466,6 +467,7 @@ def nearly? other # @param other [Object] def == other return false unless nearly? other + # @sg-ignore Should add more explicit type check on other comments == other.comments && location == other.location end diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index da1f15cb2..93b568647 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -106,6 +106,7 @@ def probe api_map # @param other [Object] def == other return false unless super + # @sg-ignore Should add type check on other assignment == other.assignment end diff --git a/lib/solargraph/pin/block.rb b/lib/solargraph/pin/block.rb index 227bc0873..0c6ecd258 100644 --- a/lib/solargraph/pin/block.rb +++ b/lib/solargraph/pin/block.rb @@ -54,6 +54,7 @@ def typify_parameters(api_map) locals = clip.locals - [self] meths = chain.define(api_map, closure, locals) # @todo Convert logic to use signatures + # @param meth [Pin::Method] meths.each do |meth| next if meth.block.nil? diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index 207c2619b..edbc3f941 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -67,6 +67,8 @@ def generics # @return [Array] def choose_parameters(other) raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity + # @param param [Pin::Parameter] + # @param other_param [Pin::Parameter] parameters.zip(other.parameters).map do |param, other_param| if param.nil? && other_param.block? other_param diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 011f096f6..86bf1cd09 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -34,14 +34,18 @@ 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{Array => Array}] by_arity = {} signature_pins.each do |signature_pin| by_arity[signature_pin.arity] ||= [] by_arity[signature_pin.arity] << signature_pin end by_arity.transform_values! do |same_arity_pins| + # @param memo [Pin::Signature, nil] + # @param signature [Pin::Signature] same_arity_pins.reduce(nil) do |memo, signature| next signature if memo.nil? memo.combine_with(signature) @@ -88,6 +92,7 @@ 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), @@ -376,11 +381,14 @@ def attribute? @attribute end - # @parm other [Method] + # @parm other [self] def nearly? other super && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 parameters == other.parameters && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 scope == other.scope && + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 visibility == other.visibility end @@ -392,9 +400,12 @@ def probe api_map def overloads # Ignore overload tags with nil parameters. If it's not an array, the # tag's source is likely malformed. + + # @param tag [YARD::Tags::OverloadTag] @overloads ||= docstring.tags(:overload).select(&:parameters).map do |tag| Pin::Signature.new( generics: generics, + # @param src [Array(String, String)] parameters: tag.parameters.map do |src| name, decl = parse_overload_param(src.first) Pin::Parameter.new( @@ -507,6 +518,7 @@ def clean_param name # # @return [ComplexType] def param_type_from_name(tag, name) + # @param t [YARD::Tags::Tag] param = tag.tags(:param).select { |t| t.name == name }.first return ComplexType::UNDEFINED unless param ComplexType.try_parse(*param.types) @@ -522,8 +534,12 @@ def generate_complex_type # @param api_map [ApiMap] # @return [ComplexType, nil] def see_reference api_map + # This should actually be an intersection type + # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] 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 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 947513689..91c205921 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -232,8 +232,12 @@ def typify_method_param api_map # @param skip [::Array] # @return [::Array] def see_reference heredoc, api_map, skip = [] + # This should actually be an intersection type + # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] 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 result = resolve_reference(ref.owner.to_s, api_map, skip) return result unless result.nil? end diff --git a/lib/solargraph/pin/search.rb b/lib/solargraph/pin/search.rb index f92978a35..0f9883b65 100644 --- a/lib/solargraph/pin/search.rb +++ b/lib/solargraph/pin/search.rb @@ -42,6 +42,9 @@ def do_query Result.new(match, pin) if match > 0.7 end .compact + # @param a [self] + # @param b [self] + # @sg-ignore https://github.com/castwide/solargraph/pull/1050 .sort { |a, b| b.match <=> a.match } .map(&:pin) end diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 1197038ef..eed62561b 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -111,6 +111,7 @@ 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 7a9bc0e30..86452d646 100644 --- a/lib/solargraph/range.rb +++ b/lib/solargraph/range.rb @@ -27,9 +27,12 @@ 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 @@ -98,6 +101,7 @@ 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/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 3e777f726..54bca0f73 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -65,6 +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 type_aliases[decl.name.to_s] = decl when RBS::AST::Declarations::Module module_decl_to_pin decl @@ -426,6 +427,7 @@ def method_def_to_pin decl, closure, context # @param pin [Pin::Method] # @return [void] def method_def_to_sigs decl, pin + # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| type_location = location_decl_to_pin_location(overload.method_type.location) generics = overload.method_type.type_params.map(&:name).map(&:to_s) @@ -466,12 +468,16 @@ def parts_of_function type, pin parameters = [] arg_num = -1 type.type.required_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" + # @sg-ignore RBS generic type understanding issue parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) end type.type.optional_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, type_location: type_location, source: :rbs) @@ -489,18 +495,23 @@ def parts_of_function type, pin return_type: rest_positional_type,) end type.type.trailing_positionals.each do |param| + # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs, type_location: type_location) end type.type.required_keywords.each do |orig, param| + # @sg-ignore RBS generic type understanding issue name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) end type.type.optional_keywords.each do |orig, param| + # @sg-ignore RBS generic type understanding issue name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, type_location: type_location, source: :rbs) @@ -792,7 +803,9 @@ def other_type_to_tag type # @param namespace [Pin::Namespace] # @return [void] def add_mixins decl, namespace + # @param mixin [RBS::AST::Members::Include, RBS::AST::Members::Members::Extend, RBS::AST::Members::Members::Prepend] decl.each_mixin do |mixin| + # @todo are we handling prepend correctly? klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend type = build_type(mixin.name, mixin.args) generic_values = type.all_params.map(&:to_s) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 046a74296..bd0e5026d 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -79,6 +79,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 @@ -273,6 +274,7 @@ def pin path else pins = api_map.get_path_pins path end + # @type [Hash{Symbol => Pin::Base}] references = {} pin = pins.first case pin diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index c08d04878..f7a03b552 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -264,6 +264,8 @@ def infer_from_definitions pins, context, api_map, locals ComplexType::UNDEFINED elsif types.length > 1 # Move nil to the end by convention + + # @param a [ComplexType::UniqueType] sorted = types.flat_map(&:items).sort { |a, _| a.tag == 'nil' ? 1 : 0 } ComplexType.new(sorted.uniq) else diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 24d10656d..194771c1f 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -266,6 +266,7 @@ def yield_pins api_map, name_pin method_pin = find_method_pin(name_pin) return [] unless method_pin + # @param signature_pin [Pin::Signature] method_pin.signatures.map(&:block).compact.map do |signature_pin| return_type = signature_pin.return_type.qualify(api_map, *name_pin.gates) signature_pin.proxy(return_type) 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/link.rb b/lib/solargraph/source/chain/link.rb index bcd9eb196..344f7affd 100644 --- a/lib/solargraph/source/chain/link.rb +++ b/lib/solargraph/source/chain/link.rb @@ -38,7 +38,7 @@ def constant? # @param api_map [ApiMap] # @param name_pin [Pin::Base] - # @param locals [::Enumerable] + # @param locals [::Array] # @return [::Array] def resolve api_map, name_pin, locals [] 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 4600767b5..996830c4d 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -460,7 +460,9 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi else ptype = data[:qualified] unless ptype.undefined? + # @sg-ignore https://github.com/castwide/solargraph/pull/1127 argtype = argchain.infer(api_map, closure_pin, locals) + # @sg-ignore 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 d8e3b8b43..0838b9f4f 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -11,7 +11,7 @@ module ToMethod ["Rails::Engine", :class, "find_root_with_flag"] => :public } - # @param code_object [YARD::CodeObjects::Base] + # @param code_object [YARD::CodeObjects::MethodObject] # @param name [String, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] @@ -85,6 +85,7 @@ 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 3ecb7ac26..010db89a5 100644 --- a/lib/solargraph/yard_map/to_method.rb +++ b/lib/solargraph/yard_map/to_method.rb @@ -15,6 +15,7 @@ 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, @@ -57,7 +58,7 @@ def arg_type a include Helpers - # @param code_object [YARD::CodeObjects::Base] + # @param code_object [YARD::CodeObjects::MethodObject] # @param name [String, nil] # @param scope [Symbol, nil] # @param visibility [Symbol, nil] diff --git a/solargraph.gemspec b/solargraph.gemspec index 49265f9c6..0414f27d8 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -2,6 +2,7 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib' require 'solargraph/version' require 'date' +# @param s [Gem::Specification] Gem::Specification.new do |s| s.name = 'solargraph' s.version = Solargraph::VERSION From 1594b958e2362d2a03c0e31d6bb178327cc7d6f2 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 16:15:19 -0500 Subject: [PATCH 326/333] Annotations needed after https://github.com/castwide/solargraph/pull/1130 --- lib/solargraph/api_map/store.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/api_map/store.rb b/lib/solargraph/api_map/store.rb index 32fa8bfd4..f86080d12 100644 --- a/lib/solargraph/api_map/store.rb +++ b/lib/solargraph/api_map/store.rb @@ -247,6 +247,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? + # @param ref [String] refs.map(&:parametrized_tag).map(&:to_s).each do |ref| next if ref.nil? || ref.empty? || visited.include?(ref) ancestors << ref From 405d9bef1d3c1071cfaf4cadb5ce8f054352a5f9 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 28 Nov 2025 17:59:43 -0500 Subject: [PATCH 327/333] 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 328/333] 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 329/333] 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 140137336004925f5fc597dbd7af4397dfb7d857 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 14:15:29 -0500 Subject: [PATCH 330/333] Fix type --- lib/solargraph/api_map/index.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solargraph/api_map/index.rb b/lib/solargraph/api_map/index.rb index 944f02c79..ad96fd759 100644 --- a/lib/solargraph/api_map/index.rb +++ b/lib/solargraph/api_map/index.rb @@ -138,7 +138,7 @@ def catalog new_pins # @generic T # @param klass [Class>] - # @param hash [Hash{String => generic}] + # @param hash [Hash{String => Array>}] # # @return [void] def map_references klass, hash From d27a522d7757f35eaeec0a24fb552beb0a9a086a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 14:36:25 -0500 Subject: [PATCH 331/333] Fix annotations --- lib/solargraph/library.rb | 2 -- lib/solargraph/pin/delegated_method.rb | 1 - lib/solargraph/pin/method.rb | 6 +++--- lib/solargraph/pin/parameter.rb | 2 +- lib/solargraph/rbs_map/conversions.rb | 5 ++++- lib/solargraph/type_checker.rb | 3 --- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index b0afaad4e..5c7851201 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -303,9 +303,7 @@ def locate_ref location return nil if pin.nil? # @param full [String] return_if_match = proc do |full| - # @sg-ignore Need to handle restarg parameters in block parameters if source_map_hash.key?(full) - # @sg-ignore Need to handle restarg parameters in block parameters return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) end end diff --git a/lib/solargraph/pin/delegated_method.rb b/lib/solargraph/pin/delegated_method.rb index 530804747..bcf5b5912 100644 --- a/lib/solargraph/pin/delegated_method.rb +++ b/lib/solargraph/pin/delegated_method.rb @@ -51,7 +51,6 @@ def type_location %i[typify realize infer probe].each do |method| # @param api_map [ApiMap] define_method(method) do |api_map| - # @sg-ignore Need to handle restarg parameters in block parameters resolve_method(api_map) # @sg-ignore Need to set context correctly in define_method blocks @resolved_method ? @resolved_method.send(method, api_map) : super(api_map) diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index b942193a9..260377a48 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -536,11 +536,11 @@ def generate_complex_type # @return [ComplexType, nil] def see_reference api_map # This should actually be an intersection type - # @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag] + # @param ref [YARD::Tags::Tag, YARD::Tags::RefTag] docstring.ref_tags.each do |ref| - # @sg-ignore ref should actually be an intersection type + # @todo 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 dc1f15440..6051f77af 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -269,7 +269,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 9bd966bda..a8ba388ac 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -738,7 +738,9 @@ def type_tag(type_name, type_args = []) build_type(type_name, type_args).tags end - # @param type [RBS::Types::Bases::Base] + # @param type [RBS::Types::Bases::Base,Object] RBS type object. + # Note: Generally these extend from RBS::Types::Bases::Base, + # but not all. # @return [String] def other_type_to_tag type if type.is_a?(RBS::Types::Optional) @@ -795,6 +797,7 @@ def other_type_to_tag type # e.g., singleton(String) type_tag(type.name) else + # all types should include location Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" 'undefined' end diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 1d20f2547..b17e7bf85 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -789,13 +789,10 @@ def fake_args_for(pin) with_block = false # @param pin [Pin::Parameter] pin.parameters.each do |pin| - # @sg-ignore Should handle redefinition of types in simple contexts if [:kwarg, :kwoptarg, :kwrestarg].include?(pin.decl) with_opts = true - # @sg-ignore Should handle redefinition of types in simple contexts elsif pin.decl == :block with_block = true - # @sg-ignore Should handle redefinition of types in simple contexts elsif pin.decl == :restarg args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true) else From 8701af7039361113b6f48ab0b9ffb1a86b1fa63c Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 29 Nov 2025 15:11:58 -0500 Subject: [PATCH 332/333] 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 333/333] 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