Skip to content

Commit 0b95499

Browse files
committed
Implement abstract classes
1 parent a66d3fb commit 0b95499

7 files changed

Lines changed: 558 additions & 26 deletions

File tree

lib/type_toolkit.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# frozen_string_literal: true
33

44
require_relative "type_toolkit/version"
5+
require_relative "type_toolkit/abstract_class"
56
require_relative "type_toolkit/interface"
67
require_relative "type_toolkit/dsl"
78
require_relative "type_toolkit/method_def_recorder"
@@ -23,5 +24,23 @@ def make_interface!(mod)
2324
mod.extend(TypeToolkit::MethodDefRecorder)
2425
mod.extend(TypeToolkit::HasAbstractMethods)
2526
end
27+
28+
#: (Module[top]) -> void
29+
def make_abstract!(mod)
30+
case mod
31+
when Class
32+
# We need to save the original implementation of `new`, so we can restore it on the subclasses later.
33+
mod.singleton_class.alias_method(:__original_new_impl, :new)
34+
35+
mod.extend(TypeToolkit::AbstractClass)
36+
mod.extend(TypeToolkit::DSL)
37+
mod.extend(TypeToolkit::MethodDefRecorder)
38+
mod.extend(TypeToolkit::HasAbstractMethods)
39+
40+
mod.include(TypeToolkit::AbstractInstanceMethodReceiver)
41+
when Module
42+
raise NotImplementedError, "Abstract modules are not implemented yet."
43+
end
44+
end
2645
end
2746
end

lib/type_toolkit/abstract_class.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
module TypeToolkit
5+
# This module is extended onto every class marked `abstract!`.
6+
# Abstract classes can't be instantiated, only subclassed.
7+
# They should contain abstract methods, which must be implemented by subclasses.
8+
#
9+
# Example:
10+
#
11+
# class Widget
12+
# abstract!
13+
#
14+
# #: -> void
15+
# abstract def draw; end
16+
# end
17+
#
18+
# class Button < Widget
19+
# # @override
20+
# #: -> void
21+
# def draw
22+
# ...
23+
# end
24+
# end
25+
#
26+
# class TextField < Widget
27+
# # @override
28+
# #: -> void
29+
# def draw
30+
# ...
31+
# end
32+
# end
33+
#
34+
module AbstractClass
35+
# An override of `new` which prevents instantiation of the class.
36+
# This needs to be overridden again in subclasses, to restore the real `.new` implementation.
37+
def new(...) # :nodoc:
38+
#: self as Class[top]
39+
40+
if respond_to?(:__original_new_impl) # This is true for the abstract classes themselves, and false for their subclasses.
41+
raise CannotInstantiateAbstractClassError, "#{self.class.name} is declared as abstract; it cannot be instantiated"
42+
end
43+
44+
# This is hit in the uncommon case where a subclass of an abstract class overrides `.new` and calls `super`.
45+
super
46+
end
47+
48+
# Restores the original `.new` implementation for the direct subclasses of an abstract class.
49+
#: (Class[AbstractClass]) -> void
50+
def inherited(subclass) # :nodoc:
51+
superclass = subclass.singleton_class.superclass #: as !nil
52+
53+
if superclass.include?(TypeToolkit::AbstractClass) &&
54+
!superclass.singleton_class.include?(TypeToolkit::AbstractClass)
55+
# We only ned to restore the original `.new` implementation for the direct subclasses of the abstract class.
56+
# That's then inherited by the indirect subclasses.
57+
58+
subclass.singleton_class.alias_method(:new, :__original_new_impl)
59+
60+
# We don't need a reference to the original implementation anymore,
61+
# so let's undef it to limit namespace pollution.
62+
subclass.singleton_class.undef_method(:__original_new_impl)
63+
end
64+
65+
super
66+
end
67+
end
68+
69+
# Raised when an attempt is made to instantiate an abstract class.
70+
CannotInstantiateAbstractClassError = Class.new(Exception) # rubocop:disable Lint/InheritException
71+
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

lib/type_toolkit/has_abstract_methods.rb

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,18 @@ def __register_abstract_method(method_name) # :nodoc:
2323
def abstract_instance_methods(include_super = true)
2424
#: self as Module[HasAbstractMethods]
2525

26-
result = @__abstract_methods
26+
result = @__abstract_methods #: Set[Symbol]?
2727

28-
return result.to_a unless include_super
29-
30-
if defined?(super) && (super_abstract_methods = super)
31-
if result
32-
result.merge(super_abstract_methods)
33-
else
34-
result = super_abstract_methods
35-
end
36-
end
37-
38-
abstract_methods_in_interfaces = included_modules.flat_map do |m|
39-
m.is_a?(HasAbstractMethods) ? m.abstract_instance_methods : []
40-
end
41-
42-
if abstract_methods_in_interfaces.any?
43-
if result&.any?
44-
result.merge(abstract_methods_in_interfaces)
45-
else
46-
result = abstract_methods_in_interfaces
28+
if include_super
29+
ancestors.each do |m|
30+
methods = m.instance_variable_get(:@__abstract_methods)
31+
if methods&.any?
32+
if result
33+
result.merge(methods)
34+
else
35+
result = methods
36+
end
37+
end
4738
end
4839
end
4940

@@ -67,9 +58,14 @@ def abstract_instance_methods(include_super = true)
6758
def abstract_method_declared?(method_name)
6859
#: self as Module[top]
6960

70-
@__abstract_methods&.include?(method_name) ||
71-
included_modules.any? { |m| m.is_a?(HasAbstractMethods) && m.abstract_method_declared?(method_name) } ||
72-
(defined?(super) && super)
61+
# FIXME: Allocating the `ancestors` array is not great.
62+
# I tried a recursive approach, but that didn't quite work.
63+
# There is only one implementation of `abstract_method_declared?` in the ancestor chain, so there is no `super` to call.
64+
# This method always checked the ivar of the current class, which might not be set. What we actually want is to
65+
# walk up the ancestor chain, and check the ivar of each ancestor.
66+
ancestors.any? do |m|
67+
m.instance_variable_get(:@__abstract_methods)&.include?(method_name)
68+
end
7369
end
7470

7571
# Returns true if the given method is abstract, and has not been implemented.

0 commit comments

Comments
 (0)