Skip to content

Commit 48cb1ea

Browse files
committed
wip
1 parent 2269f12 commit 48cb1ea

5 files changed

Lines changed: 55 additions & 6 deletions

File tree

benchmark/inherited_impl_benchmark.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,5 @@ class Child < Parent
194194
x.compare!
195195
end
196196
end
197+
198+
# TODO: benchmark startup performance

lib/type_toolkit.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_relative "type_toolkit/dsl"
55
require_relative "type_toolkit/method_def_recorder"
66
require_relative "type_toolkit/interface"
7+
require_relative "type_toolkit/abstract_class"
78

89
# Raised when a call is made to an abstract method that never had a real implementation.
910
AbstractMethodNotImplementedError = Class.new(Exception) # rubocop:disable Lint/InheritException

lib/type_toolkit/abstract_class.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
# Raised when an attempt is made to instantiate an abstract class.
4+
CannotInstantiateAbstractClassError = Class.new(Exception) # rubocop:disable Lint/InheritException
5+
6+
module TypeToolkit
7+
module AbstractClass
8+
def new(...) # :nodoc:
9+
raise CannotInstantiateAbstractClassError, "#{self.class.name} is declared as abstract; it cannot be instantiated"
10+
end
11+
12+
def inherited(subclass)
13+
if subclass.singleton_class.superclass.include?(TypeToolkit::AbstractClass) &&
14+
!subclass.singleton_class.superclass.singleton_class.include?(TypeToolkit::AbstractClass)
15+
16+
# We only ned to restore the original `.new` implementation for the direct subclasses of the abstract class.
17+
# That's then inherited by the indirect subclasses.
18+
# TODO: test this behaviour.
19+
subclass.singleton_class.alias_method(:new, :__original_new_impl)
20+
21+
# We don't need a reference to the original implementation anymore,
22+
# so let's undef it to limit namespace pollution.
23+
subclass.singleton_class.undef_method(:__original_new_impl)
24+
end
25+
26+
super
27+
end
28+
end
29+
end

lib/type_toolkit/ext/class.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
class Class
44
def abstract!
5+
p(new)
6+
# # We need to save the original implementation of `new`, so we can restore it on the subclasses later.
7+
singleton_class.alias_method(:__original_new_impl, :new)
8+
9+
extend(TypeToolkit::AbstractClass)
510
extend(TypeToolkit::DSL)
611
extend(TypeToolkit::MethodDefRecorder)
712
extend(TypeToolkit::HasAbstractMethods)

spec/abstract_class_spec.rb

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ class AbstractClassSpec < Minitest::Spec
1010
class AbstractClass
1111
abstract!
1212

13-
abstract def m1; end
13+
def m1; end
14+
abstract(:m1)
15+
1416
abstract def m2; end
1517

1618
def concrete_method = "AbstractClass#concrete_method"
@@ -45,8 +47,7 @@ def m2 = "PartiallyInheritsItsImpl#m2"
4547

4648
describe "An abstract class" do
4749
it "cannot be instantiated" do
48-
skip "Not implemented yet"
49-
assert_raises { AbstractClass.new }
50+
assert_raises(CannotInstantiateAbstractClassError) { AbstractClass.new }
5051
end
5152

5253
describe ".abstract_instance_methods" do
@@ -83,9 +84,20 @@ def m2 = "PartiallyInheritsItsImpl#m2"
8384
@class = NonImpl
8485
end
8586

86-
it "cannot be instantiated" do
87-
skip "Not implemented yet"
88-
assert_raises { NonImpl.new }
87+
it "can be instantiated" do
88+
# ...despite not implementing all the abstract methods. This matches sorbet runtime's behaviour.
89+
#
90+
# The Sorbet static typechecker ensures that when you subclass an abstract class, you must either:
91+
# 1. Implement all of its abstract methods.
92+
# 2. Mark the subclass as abstract! as well.
93+
#
94+
# Attempting to call actually any of the abstract methods will still raise, like usual.
95+
refute_nil @class.new
96+
end
97+
98+
it "does not respond to .__original_new_impl" do
99+
# binding.irb
100+
assert_raises(NoMethodError) { @class.__original_new_impl }
89101
end
90102

91103
# describe "a method with the same name as another interface's members" do

0 commit comments

Comments
 (0)