From c4ab7ebf3b2fe5b44d2bb439b9d9e3fce845635d Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 30 Sep 2026 20:43:13 -0400 Subject: [PATCH] Compare rootedness consistently in type equality UniqueType#eql? compared the raw @rooted ivar on the left against the rooted? reader on the right. rooted? is "!can_root_name? || @rooted", so for a name that cannot be rooted -- a lowercase name such as undefined -- the reader returns true while the ivar is false. That comparison was therefore permanently false, and no undefined type was equal to any other, including to itself: a.eql?(a) returned false. Anything relying on Ruby equality of types rather than tag-string equality was affected. Two distinct undefined objects deduplicated to two entries under uniq, missed each other under Set#include?, and failed to find each other as Hash keys. UNDEFINED and UNDEFINED.erase_parameters additionally broke the eql?/hash contract outright. erase_parameters passes "rooted: rooted?", so it stores @rooted = true where the constant stores false: eql? was asymmetric between them, true in one direction and false in the other, while their hashes differed. Compare rooted? on both sides rather than both raw ivars. For a name that can be rooted the two spellings are identical; for a name that cannot, rootedness carries no meaning, so two such types are the same type whichever flag they were constructed with. hash has to move with eql? or that erase_parameters pair becomes equal with differing hashes, so it now reads rooted? too. --- lib/solargraph/complex_type/unique_type.rb | 4 ++-- spec/complex_type_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4bbdda5b2d..0f40ec268d 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -190,7 +190,7 @@ def eql? other # @sg-ignore flow sensitive typing should support .class == .class @subtypes == other.subtypes && # @sg-ignore flow sensitive typing should support .class == .class - @rooted == other.rooted? && + rooted? == other.rooted? && # @sg-ignore flow sensitive typing should support .class == .class @all_params == other.all_params && # @sg-ignore flow sensitive typing should support .class == .class @@ -273,7 +273,7 @@ def conforms_to? api_map, expected, situation, rules = [], end def hash - [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash + [self.class, @name, @key_types, @sub_types, rooted?, @all_params, @parameters_type].hash end # @return [self] diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 3096065614..516c509b27 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -765,4 +765,26 @@ def make_bar expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end end + + context 'with Ruby equality of types' do + it 'considers two parses of a name that cannot be rooted equal' do + a = Solargraph::ComplexType.parse('undefined').items.first + b = Solargraph::ComplexType.parse('undefined').items.first + expect(a).to eql(b) + expect(a.hash).to eq(b.hash) + end + + it 'treats the rooted flag as meaningless on a name that cannot be rooted' do + unrooted = Solargraph::ComplexType::UniqueType.new('undefined', rooted: false) + rooted = Solargraph::ComplexType::UniqueType.new('undefined', rooted: true) + expect(unrooted).to eql(rooted) + expect(unrooted.hash).to eq(rooted.hash) + end + + it 'still distinguishes a name that can be rooted by its rooted flag' do + rooted = Solargraph::ComplexType::UniqueType.new('String', rooted: true) + unrooted = Solargraph::ComplexType::UniqueType.new('String', rooted: false) + expect(rooted).not_to eql(unrooted) + end + end end