Repository: ruby/rbs (Ruby)

Issue:
Incorrect "Invalid Variance" error when method type variable shadows outer type variable
Hi!

We found this case while testing some code:
```
#.rbs
class Bar[out X]
  def voice: [X] { () -> X } -> String
end
```

Running `rbs validate` results in an error, when none is expected:
```
...: Type parameter variance error: X is covariant but used as incompatible variance (RBS::InvalidVarianceAnnotationError)
```

If the outer variable (`[out X]`) is removed, then there is no error. We believe the variance is being read from `Bar` when it shouldn't be.


Discussion:
Thanks!



Patch:
diff --git a/lib/rbs/definition_builder.rb b/lib/rbs/definition_builder.rb
index 6b1b659a1..3ebbd6b44 100644
--- a/lib/rbs/definition_builder.rb
+++ b/lib/rbs/definition_builder.rb
@@ -539,8 +539,12 @@ def validate_type_params(definition, ancestors:, methods:)
 
         if method_types
           method_types.each do |method_type|
+            merged_params = type_params
+              .reject {|param| method_type.type_param_names.include?(param.name) }
+              .concat(method_type.type_params)
+
             result = calculator.in_method_type(method_type: method_type, variables: param_names)
-            validate_params_with(type_params, result: result) do |param|
+            validate_params_with(merged_params, result: result) do |param|
               raise InvalidVarianceAnnotationError.new(
                 type_name: definition.type_name,
                 param: param,
diff --git a/lib/rbs/location.rb b/lib/rbs/location.rb
deleted file mode 100644
index a3251f6ec..000000000
--- a/lib/rbs/location.rb
+++ /dev/null
@@ -1,221 +0,0 @@
-# module RBS
-#   class Location
-#     attr_reader :buffer
-#     attr_reader :start_pos
-#     attr_reader :end_pos
-
-#     def initialize(buffer, start_pos, end_pos)
-#       @buffer = buffer
-#       @start_pos = start_pos
-#       @end_pos = end_pos
-#     end
-
-#     # def self.new(buffer_ = nil, start_pos_ = nil, end_pos_ = nil)
-#     #   __skip__ = super(buffer_, start_pos_, end_pos_)
-#     # end
-
-#     # def inspect
-#     #   "#<#{self.class}:#{self.__id__} @buffer=#{buffer.name}, @pos=#{start_pos}...#{end_pos}, source='#{source.lines.first}', start_line=#{start_line}, start_column=#{start_column}>"
-#     # end
-
-#     def name
-#       buffer.name
-#     end
-
-#     def start_line
-#       start_loc[0]
-#     end
-
-#     def start_column
-#       start_loc[1]
-#     end
-
-#     def end_line
-#       end_loc[0]
-#     end
-
-#     def end_column
-#       end_loc[1]
-#     end
-
-#     def start_loc
-#       @start_loc ||= buffer.pos_to_loc(start_pos)
-#     end
-
-#     def end_loc
-#       @end_loc ||= buffer.pos_to_loc(end_pos)
-#     end
-
-#     def range
-#       start_pos...end_pos
-#     end
-
-#     def source
-#       @source ||= buffer.content[start_pos...end_pos] or raise
-#     end
-
-#     def to_s
-#       "#{name || "-"}:#{start_line}:#{start_column}...#{end_line}:#{end_column}"
-#     end
-
-#     def self.to_string(location, default: "*:*:*...*:*")
-#       location&.to_s || default
-#     end
-
-#     def ==(other)
-#       other.is_a?(Location) &&
-#         other.buffer == buffer &&
-#         other.start_pos == start_pos &&
-#         other.end_pos == end_pos
-#     end
-
-#     def +(other)
-#       if other
-#         raise "Invalid concat: buffer=#{buffer.name}, other.buffer=#{other.buffer.name}" unless other.buffer == buffer
-
-#         self.class.new(buffer: buffer,
-#                        start_pos: start_pos,
-#                        end_pos: other.end_pos)
-#       else
-#         self
-#       end
-#     end
-
-#     def concat(*others)
-#       others.each { |other| self << other }
-#       self
-#     end
-
-#     def <<(other)
-#       if other
-#         raise "Invalid concat: buffer=#{buffer.name}, other.buffer=#{other.buffer.name}" unless other.buffer == buffer
-#         @end_pos = other.end_pos
-#         @source = nil
-#         @end_loc = nil
-#       end
-#       self
-#     end
-
-#     def pred?(loc)
-#       loc.is_a?(Location) &&
-#         loc.name == name &&
-#         loc.start_pos == end_pos
-#     end
-
-#     def to_json(state = _ = nil)
-#       {
-#         start: {
-#           line: start_line,
-#           column: start_column
-#         },
-#         end: {
-#           line: end_line,
-#           column: end_column
-#         },
-#         buffer: {
-#           name: name&.to_s
-#         }
-#       }.to_json(state)
-#     end
-
-#     def with_children(required: {}, optional: {})
-#       # @type var required: Hash[Symbol, Range[Integer] | Location]
-#       # @type var optional: Hash[Symbol, Range[Integer] | Location | nil]
-
-#       this = WithChildren.new(buffer: buffer, start_pos: start_pos, end_pos: end_pos)
-
-#       req = required.transform_values do |value|
-#         case value
-#         when Location
-#           value.range
-#         else
-#           value
-#         end
-#       end
-
-#       opt = optional.transform_values do |value|
-#         case value
-#         when Location
-#           value.range
-#         else
-#           value
-#         end
-#       end
-
-#       this.required_children.merge!(req)
-#       this.optional_children.merge!(opt)
-
-#       this
-#     end
-
-#     class WithChildren < Location
-#       attr_reader :required_children, :optional_children
-
-#       def initialize(buffer, start_pos, end_pos)
-#         super(buffer, start_pos, end_pos)
-
-#         @optional_children = {}
-#         @required_children = {}
-#       end
-
-#       def initialize_copy(from)
-#         required_children.merge!(from.required_children)
-#         optional_children.merge!(from.optional_children)
-#         self
-#       end
-
-#       def [](key)
-#         case
-#         when required_children.key?(_ = key)
-#           range = required_children[_ = key]
-#           Location.new(buffer: buffer, start_pos: range.begin, end_pos: range.end)
-#         when optional_children.key?(_ = key)
-#           range = required_children[_ = key] || optional_children[_ = key]
-#           if range
-#             Location.new(buffer: buffer, start_pos: range.begin, end_pos: range.end)
-#           end
-#         else
-#           raise "Unknown key given: `#{key}`"
-#         end
-#       end
-
-#       def merge_required(hash)
-#         this = dup
-
-#         h = hash.transform_values do |value|
-#           case value
-#           when Range
-#             value
-#           when Location
-#             value.range
-#           else
-#             raise
-#           end
-#         end
-
-#         this.required_children.merge!(h)
-
-#         this
-#       end
-
-#       def merge_optional(hash)
-#         this = dup
-
-#         h = hash.transform_values do |value|
-#           case value
-#           when Range
-#             value
-#           when Location
-#             value.range
-#           else
-#             nil
-#           end
-#         end
-
-#         this.optional_children.merge!(h)
-
-#         this
-#       end
-#     end
-#   end
-# end
diff --git a/lib/rbs/location_aux.rb b/lib/rbs/location_aux.rb
index 72882ed1c..538be9f10 100644
--- a/lib/rbs/location_aux.rb
+++ b/lib/rbs/location_aux.rb
@@ -119,5 +119,17 @@ def each_required_key(&block)
         enum_for(:each_required_key)
       end
     end
+
+    def key?(name)
+      optional_key?(name) || required_key?(name)
+    end
+
+    def optional_key?(name)
+      _optional_keys.include?(name)
+    end
+
+    def required_key?(name)
+      _required_keys.include?(name)
+    end
   end
 end
diff --git a/sig/location.rbs b/sig/location.rbs
index 850a54c70..f46b0ec39 100644
--- a/sig/location.rbs
+++ b/sig/location.rbs
@@ -87,6 +87,12 @@ module RBS
     def each_required_key: () { (Symbol) -> void } -> void
                          | () -> Enumerator[Symbol, void]
 
+    def optional_key?: (Symbol) -> bool
+
+    def required_key?: (Symbol) -> bool
+
+    def key?: (Symbol) -> bool
+
     private
 
     def _start_loc: () -> Buffer::loc?
diff --git a/sig/method_types.rbs b/sig/method_types.rbs
index b7d9f4d8f..dd03f2b8c 100644
--- a/sig/method_types.rbs
+++ b/sig/method_types.rbs
@@ -7,7 +7,11 @@ module RBS
     # ^^^                      type_params
     #     ^^^^^^^^^^^^^^^^^^^  type
     #
-    type loc = Location[:type, :type_params]
+    type def_loc = Location[:type, :type_params]
+
+    type attr_loc = AST::Members::Attribute::loc
+
+    type loc = def_loc | attr_loc
 
     attr_reader type_params: Array[AST::TypeParam]
     attr_reader type: Types::Function


Test patch:
diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb
index a0f2ccf16..ad7006b40 100644
--- a/test/rbs/cli_test.rb
+++ b/test/rbs/cli_test.rb
@@ -278,6 +278,20 @@ def bar: [X < _Foo[Y], Y < _Bar[Z], Z < _Baz[X]] () -> void
     end
   end
 
+  def test_validate_878
+    with_cli do |cli|
+      Dir.mktmpdir do |dir|
+        (Pathname(dir) + 'a.rbs').write(<<~RBS)
+        class Bar[out X]
+          def voice: [X] { () -> X } -> String
+        end
+        RBS
+
+        cli.run(["-I", dir, "validate"])
+      end
+    end
+  end
+
   def test_constant
     with_cli do |cli|
       cli.run(%w(-r set constant Pathname))
diff --git a/test/rbs/type_parsing_test.rb b/test/rbs/type_parsing_test.rb
index c71465d59..a4cc9eb95 100644
--- a/test/rbs/type_parsing_test.rb
+++ b/test/rbs/type_parsing_test.rb
@@ -614,6 +614,7 @@ def test_type_var
       assert_equal 1, type.args.size
       type.args[0].yield_self do |arg|
         assert_instance_of Types::Variable, arg
+        assert_instance_of RBS::Location, arg.location
       end
     end