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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -4
  3. data/README.md +61 -0
  4. data/Rakefile +0 -7
  5. data/benchmark/.rubocop.yml +7 -0
  6. data/benchmark/abstract_methods_benchmark.rb +221 -0
  7. data/benchmark/interface_startup_performance.rb +128 -0
  8. data/benchmark/module_benchmark.rb +62 -0
  9. data/config/default.yml +5 -0
  10. data/docs/design/inherited_implementation_problem.md +258 -0
  11. data/docs/design/self_dot_methods.md +79 -0
  12. data/lib/rubocop/cop/type_toolkit/plugin.rb +1 -1
  13. data/lib/rubocop/cop/type_toolkit/prefer_not_nil.rb +150 -0
  14. data/lib/rubocop-type_toolkit.rb +1 -0
  15. data/lib/type_toolkit/abstract_method_receiver.rb +39 -0
  16. data/lib/type_toolkit/dsl.rb +66 -0
  17. data/lib/type_toolkit/ext/method.rb +11 -0
  18. data/lib/type_toolkit/ext/module.rb +7 -0
  19. data/lib/type_toolkit/ext/nil_assertions.rb +3 -1
  20. data/lib/type_toolkit/has_abstract_methods.rb +108 -0
  21. data/lib/type_toolkit/interface.rb +38 -0
  22. data/lib/type_toolkit/method_def_recorder.rb +53 -0
  23. data/lib/type_toolkit/method_patch.rb +16 -0
  24. data/lib/type_toolkit/version.rb +1 -1
  25. data/lib/type_toolkit.rb +3 -0
  26. data/sorbet/config +3 -1
  27. data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
  28. data/sorbet/rbi/gems/{erb@6.0.1.rbi → erb@6.0.1.1.rbi} +2 -2
  29. data/sorbet/rbi/gems/{json@2.18.1.rbi → json@2.19.9.rbi} +115 -161
  30. data/sorbet/rbi/gems/minitest@5.27.0.rbi +707 -0
  31. data/sorbet/rbi/gems/rexml@3.4.4.rbi +0 -167
  32. data/sorbet/rbi/gems/rubocop-ast@1.49.0.rbi +6 -0
  33. data/sorbet/rbi/gems/rubocop@1.84.2.rbi +7 -0
  34. data/sorbet/rbi/gems/tapioca@0.17.10.rbi +1 -0
  35. data/sorbet/rbi/gems/{yard@0.9.38.rbi → yard@0.9.44.rbi} +1206 -241
  36. data/sorbet/rbi/shims/core.rbi +19 -0
  37. data/sorbet/rbi/shims/minitest.rbi +5 -2
  38. data/sorbet/rbi/shims/rubocop_minitest.rbi +5 -2
  39. data/spec/interface_spec.rb +419 -0
  40. data/spec/misc/how_method_added_hooks_work_spec.rb +63 -0
  41. data/spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb +464 -0
  42. data/spec/spec_helper.rb +14 -0
  43. 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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module TypeToolkit
5
- VERSION = "0.0.5"
5
+ VERSION = "0.0.7"
6
6
  end
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
@@ -3,5 +3,7 @@
3
3
  --ignore=tmp/
4
4
  --ignore=vendor/
5
5
  --enable-experimental-rbs-comments
6
+ --enable-experimental-requires-ancestor
6
7
  --suppress-payload-superclass-redefinition-for=RDoc::Markup::Heading
7
- --disable-watchman
8
+ --suppress-error-code=5022
9
+ --parser=prism