Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions lib/tapioca/runtime/generic_type_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay to allow everything while running tapioca dsl? There's a similar comment on create_generic_type below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the full rationale already lives on create_generic_type's <= ("we act like all subtype checks pass", for serialization rather than runtime enforcement), so I left this as a one-line cross-reference to it instead of duplicating. And yes, allowing everything is fine here since tapioca dsl only serializes sigs, it doesn't enforce them at runtime.

end

class << self
Expand Down
27 changes: 27 additions & 0 deletions spec/tapioca/runtime/generic_type_registry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading