From bf9a2c08b0fbb8605c903e6b9299a7cce7ddf81c Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 15:03:02 -0400 Subject: [PATCH 1/9] Convert RBS implicit nil annotations --- lib/solargraph/rbs_map/conversions.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 3caf13162..e70929774 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -552,16 +552,17 @@ def method_def_to_pin decl, closure, context # @param pin [Pin::Method] # @return [void] def method_def_to_sigs decl, pin + implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| # @sg-ignore Wrong argument type for Solargraph::RbsMap::Conversions#location_decl_to_pin_location: # location expected RBS::Location, nil, received RBS::Location<:type, :type_params>, RBS::AST::Members::Attribute::loc, nil type_location = location_decl_to_pin_location(overload.method_type.location) generics = type_parameter_names(overload.method_type) - signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) + signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin, implicit_nil) rbs_block = overload.method_type.block block = if rbs_block - block_parameters, block_return_type = parts_of_function(rbs_block, pin) + block_parameters, block_return_type = parts_of_function(rbs_block, pin, implicit_nil) Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, type_location: type_location, closure: pin) @@ -588,14 +589,15 @@ def location_decl_to_pin_location location # @param type [RBS::MethodType, RBS::Types::Block] # @param pin [Pin::Method] + # @param implicit_nil [Boolean] # @return [Array(Array, ComplexType)] - def parts_of_function type, pin + def parts_of_function type, pin, implicit_nil type_location = pin.type_location if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) return [ [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, type_location: type_location)], - method_type_to_type(type) + method_type_to_type(type, implicit_nil) ] end @@ -659,7 +661,7 @@ def parts_of_function type, pin source: :rbs, type_location: type_location) end - return_type = method_type_to_type(type) + return_type = method_type_to_type(type, implicit_nil) [parameters, return_type] end @@ -843,8 +845,10 @@ def alias_to_pin decl, closure # @param type [RBS::MethodType, RBS::Types::Block] # @return [ComplexType, ComplexType::UniqueType] - def method_type_to_type type - other_type_to_type type.type.return_type + def method_type_to_type type, implicit_nil + tag = other_type_to_type type.type.return_type + return ComplexType.parse(tag.to_s + ', nil') if tag && implicit_nil + tag end # @param type [RBS::Types::Bases::Base,Object] RBS type object. From 67493f973ea5e4b5424cce42abdf485795f65a25 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 17:20:38 -0400 Subject: [PATCH 2/9] Specs --- spec/parser/flow_sensitive_typing_spec.rb | 2 +- spec/pin/method_spec.rb | 2 +- spec/source/chain/call_spec.rb | 2 +- spec/source_map/clip_spec.rb | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index cee6afef1..a277d69f9 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -954,7 +954,7 @@ def a b api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [6, 10]) - expect(clip.infer.to_s).to eq('String') + expect(clip.infer.to_s).to eq('String, nil') clip = api_map.clip_at('test.rb', [7, 17]) expect(clip.infer.to_s).to eq('nil') diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index c109746af..4e51bc03d 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -368,7 +368,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first type = pin.probe(api_map) - expect(type.to_s).to eq('String') + expect(type.to_s).to eq('String, nil') end it 'infers from multiple-assignment chains' do diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 122cc2ed7..8bab98d97 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -445,7 +445,7 @@ def foo(params) 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, foo_pin, api_map.source_map('test.rb').locals) - expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer') + expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer, nil') end it 'preserves undefined and underdefined tyypes in resolution' do diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 67b3085b2..6dadad186 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1129,7 +1129,7 @@ def bar opts = {} clip = map.clip_at('test.rb', Solargraph::Position.new(4, 15)) expect(clip.infer.to_s).to eq('Array, nil') clip = map.clip_at('test.rb', Solargraph::Position.new(5, 15)) - expect(clip.infer.to_s).to eq('String') + expect(clip.infer.to_s).to eq('String, nil') end it 'infers overloads with splats' do @@ -1779,7 +1779,7 @@ def foo; end api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [5, 6]) type = clip.infer - expect(type.to_s).to eq('Enumerable') + expect(type.to_s).to eq('Enumerable, nil') clip = api_map.clip_at('test.rb', [7, 6]) type = clip.infer expect(type.to_s).to eq('String, nil') From 66f7ef92f30c6a34000865a0aa6968ff9cf10dde Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 20:02:49 -0400 Subject: [PATCH 3/9] Linting --- lib/solargraph/rbs_map/conversions.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index e70929774..f3a5ec909 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -552,7 +552,9 @@ def method_def_to_pin decl, closure, context # @param pin [Pin::Method] # @return [void] def method_def_to_sigs decl, pin + # rubocop:disable Style/SafeNavigationChainLength implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') + # rubocop:enable Style/SafeNavigationChainLength # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| # @sg-ignore Wrong argument type for Solargraph::RbsMap::Conversions#location_decl_to_pin_location: @@ -847,7 +849,7 @@ def alias_to_pin decl, closure # @return [ComplexType, ComplexType::UniqueType] def method_type_to_type type, implicit_nil tag = other_type_to_type type.type.return_type - return ComplexType.parse(tag.to_s + ', nil') if tag && implicit_nil + return ComplexType.parse("#{tag.to_s}, nil") if tag && implicit_nil tag end From cba81e25a7d9d73909d2fe532d39a0d7cbc20bdd Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 20:06:44 -0400 Subject: [PATCH 4/9] Minor linting --- 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 f3a5ec909..5cff3dce8 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -849,7 +849,7 @@ def alias_to_pin decl, closure # @return [ComplexType, ComplexType::UniqueType] def method_type_to_type type, implicit_nil tag = other_type_to_type type.type.return_type - return ComplexType.parse("#{tag.to_s}, nil") if tag && implicit_nil + return ComplexType.parse("#{tag}, nil") if tag && implicit_nil tag end From 266f04d606cbcc4969a756129a9c2e5622425541 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 20:28:24 -0400 Subject: [PATCH 5/9] Documentation --- lib/solargraph/rbs_map/conversions.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 5cff3dce8..6d44eeef9 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -846,6 +846,7 @@ def alias_to_pin decl, closure end # @param type [RBS::MethodType, RBS::Types::Block] + # @param implicit_nil [Boolean] # @return [ComplexType, ComplexType::UniqueType] def method_type_to_type type, implicit_nil tag = other_type_to_type type.type.return_type From 474f32d09b7264962294fa974484f0d4a0c8c539 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Mon, 18 May 2026 20:36:15 -0400 Subject: [PATCH 6/9] Typecheck fix --- 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 6d44eeef9..c819136cd 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -553,7 +553,7 @@ def method_def_to_pin decl, closure, context # @return [void] def method_def_to_sigs decl, pin # rubocop:disable Style/SafeNavigationChainLength - implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') + implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') || false # rubocop:enable Style/SafeNavigationChainLength # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| From 7db9b52d9969646ddc0fda51ff1e82e5d8593a65 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 19 May 2026 13:26:41 -0400 Subject: [PATCH 7/9] Typecheck fixes --- lib/solargraph/workspace.rb | 3 ++- lib/solargraph/workspace/gemspecs.rb | 6 +++--- lib/solargraph/yard_map/cache.rb | 2 +- lib/solargraph/yard_map/mapper/to_method.rb | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index d3346c9b4..9bd691fe6 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -111,7 +111,7 @@ def has_file? filename # Get a source by its filename. # # @param filename [String] - # @return [Solargraph::Source] + # @return [Solargraph::Source, nil] def source filename source_hash[filename] end @@ -155,6 +155,7 @@ def find_gem name, version = nil, out: nil # @param updater [Source::Updater] # @return [void] def synchronize! updater + # @sg-ignore nil errors in strong checks source_hash[updater.filename] = source_hash[updater.filename].synchronize(updater) end diff --git a/lib/solargraph/workspace/gemspecs.rb b/lib/solargraph/workspace/gemspecs.rb index 849da9368..c4ce641e9 100644 --- a/lib/solargraph/workspace/gemspecs.rb +++ b/lib/solargraph/workspace/gemspecs.rb @@ -56,7 +56,6 @@ def resolve_require require ].compact.uniq # @param gem_name [String] gem_names_to_try.each do |gem_name| - # @sg-ignore Unresolved call to == on Boolean gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name } # @sg-ignore flow sensitive typing should be able to handle redefinition return [gemspec_or_preference(gemspec)] if gemspec @@ -100,11 +99,9 @@ def stdlib_dependencies stdlib_name # # @return [Gem::Specification, nil] def find_gem name, version = nil, out: $stderr - # @sg-ignore flow sensitive typing should be able to handle redefinition specish = all_gemspecs_from_bundle.find { |specish| specish.name == name && specish.version == version } return to_gem_specification specish if specish - # @sg-ignore flow sensitive typing should be able to handle redefinition specish = all_gemspecs_from_bundle.find { |specish| specish.name == name } # @sg-ignore flow sensitive typing needs to create separate ranges for postfix if return to_gem_specification specish if specish @@ -136,6 +133,7 @@ def fetch_dependencies gemspec, out: $stderr # RBS tracks implicit dependencies, like how the YAML standard # library implies pulling in the psych library. stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || [] + # @sg-ignore assuming dep values are not nil stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact (gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name) end @@ -347,8 +345,10 @@ def preference_map # @return [Gem::Specification] def gemspec_or_preference gemspec return gemspec unless preference_map.key?(gemspec.name) + # @sg-ignore preference_map[gemspec.name] is not nil return gemspec if gemspec.version == preference_map[gemspec.name].version + # @sg-ignore preference_map[gemspec.name] is not nil change_gemspec_version gemspec, preference_map[gemspec.name].version end diff --git a/lib/solargraph/yard_map/cache.rb b/lib/solargraph/yard_map/cache.rb index 82e578d4a..2a9c73e49 100644 --- a/lib/solargraph/yard_map/cache.rb +++ b/lib/solargraph/yard_map/cache.rb @@ -16,7 +16,7 @@ def set_path_pins path, pins end # @param path [String] - # @return [Array] + # @return [Array, nil] def get_path_pins path @path_pins[path] end diff --git a/lib/solargraph/yard_map/mapper/to_method.rb b/lib/solargraph/yard_map/mapper/to_method.rb index 726e920f2..2002ee6d9 100644 --- a/lib/solargraph/yard_map/mapper/to_method.rb +++ b/lib/solargraph/yard_map/mapper/to_method.rb @@ -107,9 +107,9 @@ def get_parameters code_object, location, comments, pin end # @param a [Array] - # @return [String] + # @return [String, nil] def arg_name a - a[0].gsub(/[^a-z0-9_]/i, '') + a[0]&.gsub(/[^a-z0-9_]/i, '') end # @param a [Array] From 16aff41c535ea6c71abd6e2de1660a950d1c07d5 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 19 May 2026 13:55:15 -0400 Subject: [PATCH 8/9] Typecheck fixes --- lib/solargraph/source_map.rb | 7 ++++--- lib/solargraph/source_map/mapper.rb | 8 ++++---- lib/solargraph/type_checker.rb | 26 ++++++++++++++------------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/solargraph/source_map.rb b/lib/solargraph/source_map.rb index 18f623993..e54b5604f 100644 --- a/lib/solargraph/source_map.rb +++ b/lib/solargraph/source_map.rb @@ -109,7 +109,7 @@ def cursor_at position end # @param path [String] - # @return [Pin::Base] + # @return [Pin::Base, nil] def first_pin path pins.select { |p| p.path == path }.first end @@ -123,14 +123,14 @@ def locate_pins location # @param line [Integer] # @param character [Integer] - # @return [Pin::Method,Pin::Namespace] + # @return [Pin::Method, Pin::Namespace, nil] def locate_named_path_pin line, character _locate_pin line, character, Pin::Namespace, Pin::Method end # @param line [Integer] # @param character [Integer] - # @return [Pin::Closure] + # @return [Pin::Closure, nil] def locate_closure_pin line, character _locate_pin line, character, Pin::Closure end @@ -149,6 +149,7 @@ def references name def locals_at location return [] if location.filename != filename closure = locate_closure_pin(location.range.start.line, location.range.start.character) + # @sg-ignore assume closure exists locals.select { |pin| pin.visible_at?(closure, location) } end diff --git a/lib/solargraph/source_map/mapper.rb b/lib/solargraph/source_map/mapper.rb index 83695f494..aef5a4511 100644 --- a/lib/solargraph/source_map/mapper.rb +++ b/lib/solargraph/source_map/mapper.rb @@ -62,7 +62,7 @@ def pins end # @param position [Solargraph::Position] - # @return [Solargraph::Pin::Closure] + # @return [Solargraph::Pin::Closure, nil] def closure_at position # @sg-ignore Need to add nil check here pins.select { |pin| pin.is_a?(Pin::Closure) and pin.location.range.contain?(position) }.last @@ -119,7 +119,6 @@ def process_directive source_position, comment_position, directive begin src = Solargraph::Source.load_string("def #{directive.tag.name};end", @source.filename) region = Parser::Region.new(source: src, closure: namespace) - # @type [Array] method_gen_pins = Parser.process_node(src.node, region).first.select { |pin| pin.is_a?(Pin::Method) } gen_pin = method_gen_pins.last return if gen_pin.nil? @@ -166,7 +165,8 @@ def process_directive source_position, comment_position, directive pins.push method_pin method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, source: :source_map) - if pins.last.return_type.defined? + if pins.last&.return_type&.defined? + # @sg-ignore assume pins.last exists pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', pins.last.return_type.to_s.split(', '), 'value') end @@ -201,7 +201,7 @@ def process_directive source_position, comment_position, directive region = Parser::Region.new(source: src, closure: ns) # @todo These pins may need to be marked not explicit index = @pins.length - loff = if @code.lines[comment_position.line].strip.end_with?('@!parse') + loff = if @code.lines[comment_position.line]&.strip&.end_with?('@!parse') comment_position.line + 1 else comment_position.line diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 57fbf696b..c9e9bef04 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -226,9 +226,8 @@ def method_param_type_problems_for pin # @param name [String] # @param data [Hash{Symbol => BasicObject}] params.each_pair do |name, data| - # @type [ComplexType] type = data[:qualified] - if type.undefined? + if type&.undefined? result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin) end @@ -340,7 +339,7 @@ def call_problems found = nil # @type [Array] all_found = [] - until base.links.first.undefined? + until base.links.first&.undefined? # @sg-ignore Need to add nil check here all_found = base.define(api_map, closure_pin, locals) found = all_found.first @@ -353,9 +352,9 @@ 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))) && !(closest.generic? || ignored_pins.include?(found)) if closest.defined? - result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}") + 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}") + result.push Problem.new(location, "Unresolved call to #{missing.links.last&.word}") end @marked_ranges.push rng end @@ -515,7 +514,8 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi result = [] kwargs = convert_hash(argchain.node) par = sig.parameters[idx] - # @type [Solargraph::Source::Chain] + return [] unless par # @todo Safeguard for typechecking errors + # @type [Source::Chain] argchain = kwargs[par.name.to_sym] if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}') result.concat kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs) @@ -537,7 +537,7 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi end end end - elsif par.decl == :kwarg + elsif par&.decl == :kwarg result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}") end result @@ -633,7 +633,9 @@ def add_to_param_details param_details, param_names, new_param_details next unless param_names.include?(param_name) param_details[param_name] ||= {} + # @sg-ignore assuming param_details values param_details[param_name][:tagged] ||= details[:tagged] + # @sg-ignore assuming param_details values param_details[param_name][:qualified] ||= details[:qualified] end end @@ -698,7 +700,7 @@ def declared_externally? pin found = nil # @type [Array] all_found = [] - until base.links.first.undefined? + until base.links.first&.undefined? all_found = base.define(api_map, closure_pin, locals) found = all_found.first break if found @@ -721,7 +723,7 @@ def arity_problems_for pin, arguments, location return [] if r.empty? r end - results.first + results.first || [] end # @param pin [Pin::Method] @@ -743,7 +745,7 @@ def parameterized_arity_problems_for pin, parameters, arguments, location if any_splatted_call?(unchecked.map(&:node)) settled_kwargs = parameters.count(&:keyword?) else - kwargs = convert_hash(unchecked.last.node) + kwargs = convert_hash(unchecked.last&.node) if parameters.any? { |param| %i[kwarg kwoptarg].include?(param.decl) || param.kwrestarg? } if kwargs.empty? add_params += 1 @@ -755,7 +757,7 @@ def parameterized_arity_problems_for pin, parameters, arguments, location kwargs.delete param.name.to_sym settled_kwargs += 1 elsif param.decl == :kwarg - last_arg_last_link = arguments.last.links.last + last_arg_last_link = arguments.last&.links&.last return [] if last_arg_last_link.is_a?(Solargraph::Source::Chain::Hash) && last_arg_last_link.splatted? return [Problem.new(location, "Missing keyword argument #{param.name} to #{pin.path}")] end @@ -780,7 +782,7 @@ def parameterized_arity_problems_for pin, parameters, arguments, location end return [] if arguments.length - req == parameters.select { |p| %i[optarg kwoptarg].include?(p.decl) }.length return [Problem.new(location, "Too many arguments to #{pin.path}")] - elsif unchecked.length < req - settled_kwargs && (arguments.empty? || (!arguments.last.splat? && !arguments.last.links.last.is_a?(Solargraph::Source::Chain::Hash))) + elsif unchecked.length < req - settled_kwargs && (arguments.empty? || (!arguments.last&.splat? && !arguments.last&.links.last.is_a?(Solargraph::Source::Chain::Hash))) # HACK: Kernel#raise signature is incorrect in Ruby 2.7 core docs. # See https://github.com/castwide/solargraph/issues/418 unless arguments.empty? && pin.path == 'Kernel#raise' From bc3a91d9d7ed31aacb596ee51443601300953543 Mon Sep 17 00:00:00 2001 From: Fred Snyder Date: Tue, 19 May 2026 14:17:35 -0400 Subject: [PATCH 9/9] Typecheck fixes --- lib/solargraph/source.rb | 2 ++ lib/solargraph/source_map/clip.rb | 18 +++++++++++++----- lib/solargraph/type_checker.rb | 7 +++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/solargraph/source.rb b/lib/solargraph/source.rb index 94147989e..f6895110e 100644 --- a/lib/solargraph/source.rb +++ b/lib/solargraph/source.rb @@ -74,6 +74,7 @@ def from_to l1, c1, l2, c2 # @param line [Integer] # @param column [Integer] # @return [AST::Node] + # @sg-ignore assume return value is not nil def node_at line, column tree_at(line, column).first end @@ -277,6 +278,7 @@ def associated_comments # @return [Integer] def first_not_empty_from line cursor = line + # @sg-ignore while condition ensures code_lines[cursor] exists cursor += 1 while cursor < code_lines.length && code_lines[cursor].strip.empty? cursor = line if cursor > code_lines.length - 1 cursor diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 8df3f1669..4a37b470c 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -40,7 +40,7 @@ def types # @return [Completion] def complete return package_completions([]) if !source_map.source.parsed? || cursor.string? - if cursor.chain.literal? && cursor.chain.links.last.word == '' + if cursor.chain.literal? && cursor.chain.links.last&.word == '' return package_completions(api_map.get_symbols) end return Completion.new([], cursor.range) if cursor.chain.literal? @@ -118,12 +118,12 @@ def location # @return [Solargraph::Pin::Closure] def closure - @closure ||= source_map.locate_closure_pin(cursor.node_position.line, cursor.node_position.character) + @closure ||= source_map.locate_closure_pin(cursor.node_position.line, cursor.node_position.character) || Pin::ROOT_PIN end # The context at the current position. # - # @return [Pin::Base] + # @return [Pin::Base, nil] def context_pin @context_pin ||= source_map.locate_named_path_pin(cursor.node_position.line, cursor.node_position.character) end @@ -141,7 +141,7 @@ def complete_keyword_parameters next unless param.keyword? result.push Pin::KeywordParam.new(pin.location, "#{param.name}:") end - next unless !pin.parameters.empty? && pin.parameters.last.kwrestarg? + next unless !pin.parameters.empty? && pin.parameters.last&.kwrestarg? pin.docstring.tags(:param).each do |tag| next if done.include?(tag.name) done.push tag.name @@ -193,9 +193,12 @@ def code_complete result = [] result.concat complete_keyword_parameters if cursor.chain.constant? || cursor.start_of_constant? + # @sg-ignore assume no nils in method calls full = cursor.chain.links.first.word type = if cursor.chain.undefined? + # @sg-ignore assume context_pin exists cursor.chain.base.infer(api_map, context_pin, locals) + # @sg-ignore assume full exists elsif full.include?('::') && cursor.chain.links.length == 1 # @sg-ignore Need to add nil check here ComplexType.try_parse(full.split('::')[0..-2].join('::')) @@ -205,13 +208,16 @@ def code_complete ComplexType::UNDEFINED end if type.undefined? + # @sg-ignore assume full exists if full.include?('::') result.concat api_map.get_constants(full, *gates) else - result.concat api_map.get_constants('', cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) # .select { |pin| pin.name.start_with?(full) } + # @sg-ignore assume context_pin exists + result.concat api_map.get_constants('', cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) end else result.concat api_map.get_constants(type.namespace, + # @sg-ignore assume context_pin exists cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) end else @@ -219,6 +225,7 @@ def code_complete result.concat api_map.get_complex_type_methods(type, closure.binder.namespace, cursor.chain.links.length == 1) if cursor.chain.links.length == 1 if cursor.word.start_with?('@@') + # @sg-ignore assume context_pin exists 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.full_context.namespace, @@ -228,6 +235,7 @@ def code_complete end result.concat locals result.concat file_global_methods unless closure.binder.namespace.empty? + # @sg-ignore assume context_pin exists result.concat api_map.get_constants(context_pin.context.namespace, *gates) result.concat api_map.get_methods(closure.binder.namespace, scope: closure.binder.scope, visibility: %i[public private protected]) diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index c9e9bef04..91537fd68 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -294,10 +294,12 @@ def const_problems chain = Solargraph::Parser.chain(const, filename) # @sg-ignore Need to add nil check here closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column) + # @sg-ignore assume closure_pin exists closure_pin.rebind(api_map) # @sg-ignore Need to add nil check here location = Location.new(filename, rng) locals = source_map.locals_at(location) + # @sg-ignore assume closure_pin exists pins = chain.define(api_map, closure_pin, locals) if pins.empty? result.push Problem.new(location, "Unresolved constant #{Solargraph::Parser::NodeMethods.unpack_name(const)}") @@ -321,8 +323,7 @@ def call_problems # blocks in the AST include the method call as well, so the # node returned by #call_nodes_from needs to be backed out # one closure - # @todo Need to add nil check here - # @todo Should warn on nil deference here + # @sg-ignore assume closure_pin exists closure_pin = closure_pin.closure end # @sg-ignore Need to add nil check here @@ -693,6 +694,7 @@ def declared_externally? pin # @sg-ignore flow sensitive typing needs to handle "if foo.nil?" location = Location.new(filename, Range.from_node(pin.assignment)) locals = source_map.locals_at(location) + # @sg-ignore assume closure_pin exists type = chain.infer(api_map, closure_pin, locals) if type.undefined? && !rules.ignore_all_undefined? base = chain @@ -701,6 +703,7 @@ def declared_externally? pin # @type [Array] all_found = [] until base.links.first&.undefined? + # @sg-ignore assume closure_pin exists all_found = base.define(api_map, closure_pin, locals) found = all_found.first break if found