Skip to content

Commit d755cfd

Browse files
committed
Implement abstract classes
1 parent b6129c3 commit d755cfd

6 files changed

Lines changed: 428 additions & 31 deletions

File tree

lib/type_toolkit.rb

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require_relative "type_toolkit/dsl"
66
require_relative "type_toolkit/method_def_recorder"
77
require_relative "type_toolkit/interface"
8+
require_relative "type_toolkit/abstract_class"
89

910
# Raised when a call is made to an abstract method that never had a real implementation.
1011
AbstractMethodNotImplementedError = Class.new(Exception) # rubocop:disable Lint/InheritException
@@ -29,29 +30,20 @@ def __register_abstract_method(method_name) # :nodoc:
2930
# TODO: change semantics to only return methods that are actually abstract and unimplemented.
3031
#: (?bool) -> Array[Symbol]
3132
def abstract_instance_methods(include_super = true)
32-
#: self as Module[HasAbstractMethods]
33-
34-
result = @__abstract_methods
35-
36-
return result.to_a unless include_super
37-
38-
if defined?(super) && (super_abstract_methods = super)
39-
if result
40-
result.merge(super_abstract_methods)
41-
else
42-
result = super_abstract_methods
43-
end
44-
end
45-
46-
abstract_methods_in_interfaces = included_modules.flat_map do |m|
47-
m.is_a?(HasAbstractMethods) ? m.abstract_instance_methods : []
48-
end
49-
50-
if abstract_methods_in_interfaces.any?
51-
if result&.any?
52-
result.merge(abstract_methods_in_interfaces)
53-
else
54-
result = abstract_methods_in_interfaces
33+
#: self as Module[top] & HasAbstractMethods
34+
35+
result = @__abstract_methods #: Set[Symbol]?
36+
37+
if include_super
38+
ancestors.each do |m|
39+
methods = m.instance_variable_get(:@__abstract_methods)
40+
if methods&.any?
41+
if result
42+
result.merge(methods)
43+
else
44+
result = methods
45+
end
46+
end
5547
end
5648
end
5749

@@ -73,11 +65,16 @@ def abstract_instance_methods(include_super = true)
7365
#
7466
#: (Symbol) -> bool
7567
def abstract_method_declared?(method_name)
76-
#: self as Module[untyped]
77-
78-
@__abstract_methods&.include?(method_name) ||
79-
included_modules.any? { |m| m.is_a?(HasAbstractMethods) && m.abstract_method_declared?(method_name) } ||
80-
(defined?(super) && super)
68+
#: self as Module[top]
69+
70+
# FIXME: Allocating the `ancestors` array is not great.
71+
# I tried a recursive approach, but that didn't quite work.
72+
# There is only one implementation of `abstract_method_declared?` in the ancestor chain, so there is no `super` to call.
73+
# This method always checked the ivar of the current class, which might not be set. What we actually want is to
74+
# walk up the ancestor chain, and check the ivar of each ancestor.
75+
ancestors.any? do |m|
76+
m.instance_variable_get(:@__abstract_methods)&.include?(method_name)
77+
end
8178
end
8279

8380
# Returns true if the given method is abstract, and has not been implemented.
@@ -168,5 +165,24 @@ def make_interface!(mod)
168165
mod.extend(TypeToolkit::MethodDefRecorder)
169166
mod.extend(TypeToolkit::HasAbstractMethods)
170167
end
168+
169+
def make_abstract!(mod)
170+
case mod
171+
when Class
172+
# We need to save the original implementation of `new`, so we can restore it on the subclasses later.
173+
mod.singleton_class.alias_method(:__original_new_impl, :new)
174+
175+
mod.extend(TypeToolkit::AbstractClass)
176+
mod.extend(TypeToolkit::DSL)
177+
mod.extend(TypeToolkit::MethodDefRecorder)
178+
mod.extend(TypeToolkit::HasAbstractMethods)
179+
180+
mod.include(TypeToolkit::AbstractInstanceMethodReceiver)
181+
when Module
182+
raise NotImplementedError, "Abstract modules are not implemented yet."
183+
else
184+
raise TypeError, "Expected a Class or Module, got #{mod.class}."
185+
end
186+
end
171187
end
172188
end

lib/type_toolkit/abstract_class.rb

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

lib/type_toolkit/ext.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# typed: strong
22
# frozen_string_literal: true
33

4+
require "type_toolkit/ext/class"
45
require "type_toolkit/ext/method"
56
require "type_toolkit/ext/module"

lib/type_toolkit/ext/class.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class Class
4+
def abstract!
5+
TypeToolkit.make_abstract!(self)
6+
end
7+
end

0 commit comments

Comments
 (0)