type_toolkit 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -4
- data/README.md +61 -0
- data/Rakefile +0 -7
- data/benchmark/.rubocop.yml +7 -0
- data/benchmark/abstract_methods_benchmark.rb +221 -0
- data/benchmark/interface_startup_performance.rb +128 -0
- data/benchmark/module_benchmark.rb +62 -0
- data/config/default.yml +5 -0
- data/docs/design/inherited_implementation_problem.md +258 -0
- data/docs/design/self_dot_methods.md +79 -0
- data/lib/rubocop/cop/type_toolkit/plugin.rb +1 -1
- data/lib/rubocop/cop/type_toolkit/prefer_not_nil.rb +150 -0
- data/lib/rubocop-type_toolkit.rb +1 -0
- data/lib/type_toolkit/abstract_method_receiver.rb +39 -0
- data/lib/type_toolkit/dsl.rb +66 -0
- data/lib/type_toolkit/ext/method.rb +11 -0
- data/lib/type_toolkit/ext/module.rb +7 -0
- data/lib/type_toolkit/ext/nil_assertions.rb +3 -1
- data/lib/type_toolkit/has_abstract_methods.rb +108 -0
- data/lib/type_toolkit/interface.rb +38 -0
- data/lib/type_toolkit/method_def_recorder.rb +53 -0
- data/lib/type_toolkit/method_patch.rb +16 -0
- data/lib/type_toolkit/version.rb +1 -1
- data/lib/type_toolkit.rb +3 -0
- data/sorbet/config +3 -1
- data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
- data/sorbet/rbi/gems/{erb@6.0.1.rbi → erb@6.0.1.1.rbi} +2 -2
- data/sorbet/rbi/gems/{json@2.18.1.rbi → json@2.19.9.rbi} +115 -161
- data/sorbet/rbi/gems/minitest@5.27.0.rbi +707 -0
- data/sorbet/rbi/gems/rexml@3.4.4.rbi +0 -167
- data/sorbet/rbi/gems/rubocop-ast@1.49.0.rbi +6 -0
- data/sorbet/rbi/gems/rubocop@1.84.2.rbi +7 -0
- data/sorbet/rbi/gems/tapioca@0.17.10.rbi +1 -0
- data/sorbet/rbi/gems/{yard@0.9.38.rbi → yard@0.9.44.rbi} +1206 -241
- data/sorbet/rbi/shims/core.rbi +19 -0
- data/sorbet/rbi/shims/minitest.rbi +5 -2
- data/sorbet/rbi/shims/rubocop_minitest.rbi +5 -2
- data/spec/interface_spec.rb +419 -0
- data/spec/misc/how_method_added_hooks_work_spec.rb +63 -0
- data/spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb +464 -0
- data/spec/spec_helper.rb +14 -0
- metadata +24 -4
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypeToolkit
|
|
5
|
+
# Given a class Foo, abstract methods can be defined at two levels:
|
|
6
|
+
# 1. Instance methods available on the instance of Foo
|
|
7
|
+
# 2. "Class methods" available on the Foo class itself
|
|
8
|
+
#
|
|
9
|
+
# In case 1, the storage belongs one level up, in the class Foo.
|
|
10
|
+
# In case 2, the storage belongs two levels up, in the singleton class of Foo (`Foo::<Foo>`)
|
|
11
|
+
module HasAbstractMethods
|
|
12
|
+
# Private API, do not use directly. Only meant to be called from the `abstract` macro.
|
|
13
|
+
#: (Symbol) -> void
|
|
14
|
+
def __register_abstract_method(method_name) # :nodoc:
|
|
15
|
+
(
|
|
16
|
+
@__abstract_methods ||= Set.new #: Set[Symbol]?
|
|
17
|
+
) << method_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns all methods that were marked abstract (even those which are implemented).
|
|
21
|
+
#: (?bool) -> Array[Symbol]
|
|
22
|
+
def declared_abstract_instance_methods(include_super = true)
|
|
23
|
+
#: self as HasAbstractMethods & Module[top]
|
|
24
|
+
|
|
25
|
+
result = @__abstract_methods
|
|
26
|
+
|
|
27
|
+
return result.to_a unless include_super
|
|
28
|
+
|
|
29
|
+
if defined?(super) && (super_abstract_methods = super)
|
|
30
|
+
if result
|
|
31
|
+
result.merge(super_abstract_methods)
|
|
32
|
+
else
|
|
33
|
+
result = super_abstract_methods
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
abstract_methods_in_interfaces = included_modules.flat_map do |m|
|
|
38
|
+
m.is_a?(HasAbstractMethods) ? m.declared_abstract_instance_methods : []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if abstract_methods_in_interfaces.any?
|
|
42
|
+
if result&.any?
|
|
43
|
+
result.merge(abstract_methods_in_interfaces)
|
|
44
|
+
else
|
|
45
|
+
result = abstract_methods_in_interfaces
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result.to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns all methods that are abstract and have not been implemented.
|
|
53
|
+
#: (?bool) -> Array[Symbol]
|
|
54
|
+
def abstract_instance_methods(include_super = true)
|
|
55
|
+
#: self as HasAbstractMethods & Module[top]
|
|
56
|
+
|
|
57
|
+
declared_abstract_instance_methods(include_super).reject do |m|
|
|
58
|
+
method_defined?(m) || private_method_defined?(m)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns true if the given method name was ever marked abstract, even if it has a concrete implementation.
|
|
63
|
+
#
|
|
64
|
+
# Similar to `public_method_defined?` and friends, this method is called on a class to check if the method
|
|
65
|
+
# is defined for _instances_ of that class. For example:
|
|
66
|
+
#
|
|
67
|
+
# if Foo.abstract_method_declared?(:instance_method)
|
|
68
|
+
# Foo.new.instance_method # Might raise AbstractMethodNotImplementedError
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# if Foo.singleton_class.abstract_method_declared?(:class_method)
|
|
72
|
+
# Foo.class_method # Might raise AbstractMethodNotImplementedError
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
#: (Symbol) -> bool
|
|
76
|
+
def abstract_method_declared?(method_name)
|
|
77
|
+
#: self as Module[top]
|
|
78
|
+
|
|
79
|
+
@__abstract_methods&.include?(method_name) ||
|
|
80
|
+
included_modules.any? { |m| m.is_a?(HasAbstractMethods) && m.abstract_method_declared?(method_name) } ||
|
|
81
|
+
(defined?(super) && super)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns true if the given method is abstract, and has not been implemented.
|
|
85
|
+
# Calling it *will* raise an `AbstractMethodNotImplementedError`.
|
|
86
|
+
#
|
|
87
|
+
# Similar to `public_method_defined?` and friends, this method is called on a class to check if the method
|
|
88
|
+
# is defined for _instances_ of that class. For example:
|
|
89
|
+
#
|
|
90
|
+
# if Foo.abstract_method?(:instance_method)
|
|
91
|
+
# Foo.new.instance_method # Will raise AbstractMethodNotImplementedError
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# if Foo.singleton_class.abstract_method?(:class_method)
|
|
95
|
+
# Foo.class_method # Will raise AbstractMethodNotImplementedError
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
#: (Symbol) -> bool
|
|
99
|
+
def abstract_method?(method_name)
|
|
100
|
+
#: self as (HasAbstractMethods & Module[top])
|
|
101
|
+
|
|
102
|
+
# If the method is defined, it has a concrete implementation, so it's not abstract.
|
|
103
|
+
return false if method_defined?(method_name) || private_method_defined?(method_name)
|
|
104
|
+
|
|
105
|
+
abstract_method_declared?(method_name)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "dsl"
|
|
5
|
+
require_relative "method_def_recorder"
|
|
6
|
+
require_relative "has_abstract_methods"
|
|
7
|
+
require_relative "abstract_method_receiver"
|
|
8
|
+
|
|
9
|
+
module TypeToolkit
|
|
10
|
+
class << self
|
|
11
|
+
#: (Module[top]) -> void
|
|
12
|
+
def make_interface!(mod)
|
|
13
|
+
if Class === mod
|
|
14
|
+
raise TypeError, "Classes can't be interfaces. Did you mean to make it `abstract` instead?"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
mod.extend(TypeToolkit::Interface)
|
|
18
|
+
mod.extend(TypeToolkit::DSL)
|
|
19
|
+
mod.extend(TypeToolkit::MethodDefRecorder)
|
|
20
|
+
mod.extend(TypeToolkit::HasAbstractMethods)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# This module is extended onto any module that represents an interface.
|
|
25
|
+
# All of its members should be public and abstract.
|
|
26
|
+
module Interface
|
|
27
|
+
#: (Module[top]) -> void
|
|
28
|
+
def included(target_module)
|
|
29
|
+
# Including/extending a module is idempotent, so we don't have to worry these were already included/extended.
|
|
30
|
+
|
|
31
|
+
# Potentially abstract methods will be called on instances of `self`, so we need the `method_missing` hooks.
|
|
32
|
+
target_module.include(TypeToolkit::AbstractInstanceMethodReceiver)
|
|
33
|
+
|
|
34
|
+
# The `method_missing` hooks need to be able to look up the abstract methods (from the interface).
|
|
35
|
+
target_module.extend(TypeToolkit::HasAbstractMethods)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypeToolkit
|
|
5
|
+
# This module tracks new methods being defined, and whether they were
|
|
6
|
+
# instance methods (`def foo; end`) or singleton methods (`def self.foo; end`).
|
|
7
|
+
module MethodDefRecorder
|
|
8
|
+
#: [Symbol, bool]?
|
|
9
|
+
attr_reader :__last_method_def
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
#: (Module[top]) -> void
|
|
13
|
+
def extended(target_module)
|
|
14
|
+
# Also extend the singleton class so methods are available there
|
|
15
|
+
target_module.singleton_class.extend(ClassMethods)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# These actually go on the YourClass.singleton_class.singleton_class
|
|
20
|
+
# @requires_ancestor: MethodDefRecorder
|
|
21
|
+
module ClassMethods
|
|
22
|
+
#: () -> [Symbol, bool]?
|
|
23
|
+
def __last_method_def
|
|
24
|
+
#: self as Class[MethodDefRecorder]
|
|
25
|
+
cls = attached_object #: as MethodDefRecorder
|
|
26
|
+
cls.__last_method_def
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Need `@without_runtime` because of https://github.com/Shopify/tapioca/issues/2513
|
|
31
|
+
# @without_runtime
|
|
32
|
+
#: (Symbol) -> void
|
|
33
|
+
def method_added(m)
|
|
34
|
+
is_singleton_method = false
|
|
35
|
+
@__last_method_def = [m, is_singleton_method] #: [Symbol, bool]?
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Need `@without_runtime` because of https://github.com/Shopify/tapioca/issues/2513
|
|
41
|
+
# @without_runtime
|
|
42
|
+
# @override
|
|
43
|
+
#: (Symbol) -> void
|
|
44
|
+
def singleton_method_added(m)
|
|
45
|
+
return super if m == :singleton_method_added
|
|
46
|
+
|
|
47
|
+
is_singleton_method = true
|
|
48
|
+
@__last_method_def = [m, is_singleton_method] #: [Symbol, bool]?
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module TypeToolkit
|
|
5
|
+
# @requires_ancestor: MethodOrUnboundMethod
|
|
6
|
+
module MethodPatch
|
|
7
|
+
# Returns true if this method is an abstract method that hasn't been implemented.
|
|
8
|
+
# Calling it will raise an `AbstractMethodNotImplementedError`.
|
|
9
|
+
#: -> bool
|
|
10
|
+
def abstract?
|
|
11
|
+
return false unless TypeToolkit::HasAbstractMethods === (owner = self.owner)
|
|
12
|
+
|
|
13
|
+
owner.abstract_method?(name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/type_toolkit/version.rb
CHANGED
data/lib/type_toolkit.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative "type_toolkit/version"
|
|
5
|
+
require_relative "type_toolkit/interface"
|
|
6
|
+
require_relative "type_toolkit/ext/method"
|
|
7
|
+
require_relative "type_toolkit/ext/module"
|
|
5
8
|
require_relative "type_toolkit/ext/nil_assertions"
|
|
6
9
|
|
|
7
10
|
module TypeToolkit
|
data/sorbet/config
CHANGED