diff --git a/lib/tapioca/runtime/generic_type_registry.rb b/lib/tapioca/runtime/generic_type_registry.rb index 9cdb5d5ee..92e1c9939 100644 --- a/lib/tapioca/runtime/generic_type_registry.rb +++ b/lib/tapioca/runtime/generic_type_registry.rb @@ -25,11 +25,18 @@ module GenericTypeRegistry @type_variables = {}.compare_by_identity #: Hash[Module[top], Array[TypeVariableModule]] - class GenericType < T::Types::Simple + # Subclasses `T::Types::Base` rather than `T::Types::Simple` so that all + # validation goes through the `valid?` override below. `Simple` fast paths + # check `raw_type` directly, which for us is the clone, not the underlying type. + class GenericType < T::Types::Base + #: Module[top] + attr_reader :raw_type + #: (Module[top] raw_type, Module[top] underlying_type) -> void def initialize(raw_type, underlying_type) - super(raw_type) + super() + @raw_type = raw_type @underlying_type = underlying_type #: Module[top] end @@ -38,6 +45,24 @@ def initialize(raw_type, underlying_type) def valid?(obj) obj.is_a?(@underlying_type) end + + # @override + #: -> String + def name + T.must(@raw_type.name) + end + + # @override + #: -> nil + def build_type + nil + end + + # Matches the always-true `<=` we define on the clone in `create_generic_type`. + #: (T::Types::Base type) -> bool + private def subtype_of_single?(type) + true + end end class << self diff --git a/spec/tapioca/runtime/generic_type_registry_spec.rb b/spec/tapioca/runtime/generic_type_registry_spec.rb index 3f2b137f0..49996602d 100644 --- a/spec/tapioca/runtime/generic_type_registry_spec.rb +++ b/spec/tapioca/runtime/generic_type_registry_spec.rb @@ -62,6 +62,33 @@ class GenericTypeRegistrySpec < Minitest::Spec T.let(SampleGenericInterfaceImplementation.new, SampleGenericInterface[Object]) end + it "recognizes underlying-type instances in a composite T::Props member (issue #2130)" do + struct_class = T.let( + Class.new(T::Struct) do + const :members, T::Hash[Symbol, SampleGenericInterface[Object]] + end, + T.untyped, + ) + + instance = struct_class.new(members: { key: SampleGenericInterfaceImplementation.new }) + + assert_instance_of(SampleGenericInterfaceImplementation, instance.members.fetch(:key)) + end + + it "recognizes underlying-type instances in a bare T::Props member (issue #2130)" do + # A bare (non-composite) member exercises the `T::Types::Simple` setter fast path. + struct_class = T.let( + Class.new(T::Struct) do + const :member, SampleGenericInterface[Object] + end, + T.untyped, + ) + + instance = struct_class.new(member: SampleGenericInterfaceImplementation.new) + + assert_instance_of(SampleGenericInterfaceImplementation, instance.member) + end + it "does not reuse generic instances for redefined constants with the same name" do first_constant, first_generic_type = register_reloadable_generic