From 831baec18e92be4b0cc0423d038c5cc3713f827d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 03:26:21 +0000 Subject: [PATCH] Fix TypeParam.rename to substitute variables in bounds and default types The substitution in TypeParam.rename was built from the new names to themselves, making it an identity substitution. Type variables in upper bounds, lower bounds, and default types that reference other type parameters were left with the old names. One visible consequence: `validate_type_params` raised a false GenericParameterMismatchError for declarations like `module M[A, B < _Foo[A]]` and `module M[X, Y < _Foo[X]]`, which are compatible modulo renaming. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TBY8ct4HpkVkZNPdsNHEDE --- lib/rbs/ast/type_param.rb | 2 +- test/rbs/ast/type_param_test.rb | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/rbs/ast/type_param.rb b/lib/rbs/ast/type_param.rb index b4f599b94a..33238e6053 100644 --- a/lib/rbs/ast/type_param.rb +++ b/lib/rbs/ast/type_param.rb @@ -114,7 +114,7 @@ def self.subst_var(vars, type) def self.rename(params, new_names:) raise unless params.size == new_names.size - subst = Substitution.build(new_names, Types::Variable.build(new_names)) + subst = Substitution.build(params.map(&:name), Types::Variable.build(new_names)) params.map.with_index do |param, index| new_name = new_names[index] diff --git a/test/rbs/ast/type_param_test.rb b/test/rbs/ast/type_param_test.rb index 080d155837..cd6c67a263 100644 --- a/test/rbs/ast/type_param_test.rb +++ b/test/rbs/ast/type_param_test.rb @@ -66,4 +66,28 @@ def test_normalize_args assert_equal ["::Integer", "::Array[::Integer]", "::Array[::Array[::Integer]]"], args.map(&:to_s) end end + + def test_rename + params = [ + TypeParam.new(name: :A, variance: :covariant, upper_bound: nil, lower_bound: nil, location: nil), + TypeParam.new( + name: :B, + variance: :invariant, + upper_bound: parse_type("::Array[A]"), + lower_bound: nil, + default_type: parse_type("::Hash[A, B]"), + location: nil + ).unchecked! + ] + + TypeParam.rename(params, new_names: [:X, :Y]).tap do |renamed| + assert_equal [:X, :Y], renamed.map(&:name) + assert_equal :covariant, renamed[0].variance + assert_equal :invariant, renamed[1].variance + assert_predicate renamed[1], :unchecked? + + assert_equal parse_type("::Array[X]", variables: [:X]), renamed[1].upper_bound_type + assert_equal parse_type("::Hash[X, Y]", variables: [:X, :Y]), renamed[1].default_type + end + end end