Skip to content

Commit

Permalink
Ensure safety against subclass prevention methods
Browse files Browse the repository at this point in the history
Previously, we were doing special casing to ensure that we were not
caught by the `inherited` hook in a sealed class preventing us from
subclassing the generic type. However, that was short-sighted since it
only worked for sealed classes. But there are more cases where
subclassing is not allowed and they are all handled via `inherited`
hooks.

So, the generic (hah, nice pun), way of addressing this is to override
the `inherited` hook as we are creating a subclass and then placing the
method back in its place. That makes sure that no-one can throw because
we are subclassing.
  • Loading branch information
paracycle committed Apr 20, 2021
1 parent 4955d93 commit fa3c30b
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 19 deletions.
41 changes: 22 additions & 19 deletions lib/tapioca/generic_type_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def create_generic_type(constant, name)
# the generic class `Foo[Bar]` is still a `Foo`. That is:
# `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case
# if we just clone the class. But subclassing works just fine.
create_sealed_safe_subclass(constant)
create_safe_subclass(constant)
else
# This can only be a module and it is fine to just clone modules
# since they can't have instances and will not have `is_a?` relationships.
Expand Down Expand Up @@ -151,28 +151,31 @@ def register_type_variable(constant, type_variable_type, type_variable, fixed, l
end

sig { params(constant: Class).returns(Class) }
def create_sealed_safe_subclass(constant)
# If the constant is not sealed let's just bail early.
# We just return a subclass of the constant.
return Class.new(constant) unless T::Private::Sealed.sealed_module?(constant)

# Since sealed classes can normally not be subclassed, we need to trick
# sealed classes into thinking that the generic type we are
# creating by subclassing is actually safe for sealed types.
#
# Get the filename the sealed class was declared in
decl_file = constant.instance_variable_get(:@sorbet_sealed_module_decl_file)
def create_safe_subclass(constant)
# Lookup the "inherited" class method
inherited_method = constant.method(:inherited)
# and the module that defines it
owner = inherited_method.owner

# If no one has overriden the inherited method yet, just subclass
return Class.new(constant) if Class == owner

begin
# Clear the current declaration filename on the class
constant.remove_instance_variable(:@sorbet_sealed_module_decl_file)
# Make this file be the declaration filename so that Sorbet runtime
# does not shout at us for an invalid subclassing.
T.cast(constant, T::Helpers).sealed!
# Otherwise, some inherited method could be preventing us
# from creating subclasses, so let's override it and rescue
owner.send(:define_method, :inherited) do |s|
begin
inherited_method.call(s)
rescue
# Ignoring errors
end
end

# return a subclass
Class.new(constant)
ensure
# Reinstate the original declaration filename
constant.instance_variable_set(:@sorbet_sealed_module_decl_file, decl_file)
# Reinstate the original inherited method back.
owner.send(:define_method, :inherited, inherited_method)
end
end

Expand Down
61 changes: 61 additions & 0 deletions spec/tapioca/compilers/symbol_table_compiler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2432,6 +2432,67 @@ class Foo
assert_equal(output, compile)
end

it("can compile typed struct generics") do
add_ruby_file("tstruct_generic.rb", <<~RUBY)
class Foo < T::Struct
extend T::Generic
Elem = type_member
const :foo, Elem
end
Foo[Integer] # this should not trigger an error
RUBY

output = template(<<~RBI)
class Foo < ::T::Struct
extend T::Generic
Elem = type_member
const :foo, T.untyped
class << self
def inherited(s); end
end
end
RBI

assert_equal(output, compile)
end

it("can compile generics that prohibit subclasses") do
add_ruby_file("non_subclassable_generic.rb", <<~RUBY)
class Foo
extend T::Generic
Elem = type_member
def self.inherited(s)
super(s)
raise "Cannot subclass Foo"
end
end
Foo[Integer] # this should not trigger an error
RUBY

output = template(<<~RBI)
class Foo
extend T::Generic
Elem = type_member
class << self
def inherited(s); end
end
end
RBI

assert_equal(output, compile)
end

it("compiles structs with default values") do
add_ruby_file("foo.rb", <<~RUBY)
class Foo < T::Struct
Expand Down

0 comments on commit fa3c30b

Please sign in to comment.