type_toolkit 0.0.5 → 0.0.6

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -0
  3. data/benchmark/.rubocop.yml +7 -0
  4. data/benchmark/abstract_methods_benchmark.rb +221 -0
  5. data/benchmark/interface_startup_performance.rb +128 -0
  6. data/benchmark/module_benchmark.rb +62 -0
  7. data/config/default.yml +5 -0
  8. data/docs/design/inherited_implementation_problem.md +119 -0
  9. data/docs/design/self_dot_methods.md +79 -0
  10. data/lib/rubocop/cop/type_toolkit/plugin.rb +1 -1
  11. data/lib/rubocop/cop/type_toolkit/prefer_not_nil.rb +96 -0
  12. data/lib/rubocop-type_toolkit.rb +1 -0
  13. data/lib/type_toolkit/abstract_method_receiver.rb +39 -0
  14. data/lib/type_toolkit/dsl.rb +64 -0
  15. data/lib/type_toolkit/ext/method.rb +11 -0
  16. data/lib/type_toolkit/ext/module.rb +7 -0
  17. data/lib/type_toolkit/ext/nil_assertions.rb +3 -1
  18. data/lib/type_toolkit/has_abstract_methods.rb +108 -0
  19. data/lib/type_toolkit/interface.rb +38 -0
  20. data/lib/type_toolkit/method_def_recorder.rb +53 -0
  21. data/lib/type_toolkit/method_patch.rb +16 -0
  22. data/lib/type_toolkit/version.rb +1 -1
  23. data/lib/type_toolkit.rb +3 -0
  24. data/sorbet/config +3 -1
  25. data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
  26. data/sorbet/rbi/gems/{erb@6.0.1.rbi → erb@6.0.1.1.rbi} +2 -2
  27. data/sorbet/rbi/gems/{json@2.18.1.rbi → json@2.19.9.rbi} +115 -161
  28. data/sorbet/rbi/gems/minitest@5.27.0.rbi +707 -0
  29. data/sorbet/rbi/gems/rexml@3.4.4.rbi +0 -167
  30. data/sorbet/rbi/gems/rubocop-ast@1.49.0.rbi +6 -0
  31. data/sorbet/rbi/gems/rubocop@1.84.2.rbi +7 -0
  32. data/sorbet/rbi/gems/tapioca@0.17.10.rbi +1 -0
  33. data/sorbet/rbi/gems/{yard@0.9.38.rbi → yard@0.9.44.rbi} +1206 -241
  34. data/sorbet/rbi/shims/core.rbi +19 -0
  35. data/sorbet/rbi/shims/minitest.rbi +5 -2
  36. data/spec/interface_spec.rb +405 -0
  37. data/spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb +229 -0
  38. data/spec/spec_helper.rb +14 -0
  39. metadata +23 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a743f2b4d2786fb8c08c666974f6f913a4baea59dc9d99dba7a5982ec13441a
4
- data.tar.gz: '097ea19c5691a4416de13f1d49ad05544793a9246cd358fcc5619ccca39f6419'
3
+ metadata.gz: f4227c717f707bc4d21aea4b6026e4ec48c62bf48044518b7545b4a12d499662
4
+ data.tar.gz: afbee5d26bf77e402c0425dc60cfb53b6d8c80ba8af6f69af39bad034459a3bc
5
5
  SHA512:
6
- metadata.gz: 66a3f796e922912c0909a4a7548fd72f75a2cee65800a64c37189176cb6bbb323e4693c458b6bef99f828550e83693b036ef3f1e46542a4cf8da547255ed7c76
7
- data.tar.gz: 846e557b1e223a17adfc8ad6df338a9032cc3ea983e8f0079881f85f9012b0f20968ffd08f176386504393133cc63124e19a0cdb88b6124bac5452f0f20798b3
6
+ metadata.gz: a251f77a0cc924ae2b621f1eda192d24d862406f076c875227ada2f7d16883a8c98e8c8b7310843a00a7d08b2fcab902e0f7bf6bcc5c4262da519a2e7a2721d8
7
+ data.tar.gz: a93074b2e21d1ea1b7dac91b9cf8e52690f68d1a2ae12e2c898cbd87b3ea71367a4d956d13933faef666e6683d868e7360a5b66a2039c8ccd735bdeefa82ae09
data/README.md CHANGED
@@ -70,6 +70,49 @@ last_delivery = user.not_nil!
70
70
  .deliveries.last.not_nil!
71
71
  ```
72
72
 
73
+ ### Interfaces
74
+
75
+ Interfaces are modules with abstract methods which a conforming class must implement. They help make duck-typing easier to use in Ruby, by validating that your conforming classes do actually provide the correct methods needed of them.
76
+
77
+ The Type Toolkit provides runtime support for interfaces (marked with `interface!`) and abstract methods (marked with `abstract` before the `def` keyword).
78
+
79
+ Example:
80
+
81
+ ```ruby
82
+ module Notifier
83
+ interface!
84
+
85
+ #: (String) -> void
86
+ abstract def send_notification(message); end
87
+ end
88
+
89
+ class SlackNotifier
90
+ include Notifier
91
+
92
+ # @override
93
+ #: (String) -> void
94
+ def send_notification(message)
95
+ puts "Posting to Slack API: #{message.inspect}"
96
+ end
97
+ end
98
+
99
+ SlackNotifier.new.send_notification("Hello, world!") # ✅
100
+ # => Posting to Slack API: "Hello, world!"
101
+ ```
102
+
103
+ Unimplemented abstract methods cannot be called, and the Type Toolkit runtime will raise an error if you try to do so:
104
+
105
+ ```ruby
106
+ class EmailNotifier
107
+ include Notifier
108
+
109
+ # Oops, forgot to implement `#send_notification`!
110
+ end
111
+
112
+ EmailNotifier.new.send_notification("Hello, world!") # ❌ TypeToolkit::AbstractMethodNotImplementedError
113
+ # => Abstract method #send_notification was never implemented.
114
+ ```
115
+
73
116
  ## Guiding Principles
74
117
 
75
118
  ### Blazingly fast™
@@ -0,0 +1,7 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ Naming/ClassAndModuleCamelCase:
4
+ Enabled: false # Sometimes underscores are useful, m'kay?
5
+
6
+ Style/ClassMethodsDefinitions:
7
+ Enabled: false # We need to be able to compare `class << self` and `def self.`
@@ -0,0 +1,221 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark the performance overhead of calling:
5
+ # - A concrete implementation of an abstract method
6
+ # - An inherited concrete implementation of an abstract method
7
+ # - The error case of calling an unimplemented abstract method
8
+
9
+ ############################################# Results #############################################
10
+ #
11
+ # ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
12
+ #
13
+ # ## Interpreter
14
+ #
15
+ # | Call to... | Regular impl | Inherited impl | Missing impl |
16
+ # |-------------------|--------------------:|------------------------:|--------------------------:|
17
+ # | sorbet-runtime | (same-ish) 23.02 ns | (2.70x slower) 57.30 ns | (1.13x slower) 472.86 ns |
18
+ # | manual delegation | (same-ish) 22.18 ns | (2.07x slower) 44.90 ns | *415.36 ns* |
19
+ # | type_toolkit | (same-ish) 22.56 ns | *22.03 ns* | (2.11x slower) 890.38 ns |
20
+ #
21
+ # ## YJIT#
22
+ # | Call to... | Regular impl | Inherited impl | Missing impl |
23
+ # |-------------------|--------------------:|-------------------------:|--------------------------:|
24
+ # | sorbet-runtime | (same-ish) 1.63 ns | (21.41x slower) 34.91 ns | (1.10x slower) 447.59 ns |
25
+ # | manual delegation | (same-ish) 1.63 ns | (7.15x slower) 11.66 ns | *405.84 ns* |
26
+ # | type_toolkit | (same-ish) 1.67 ns | *1.63 ns* | (1.91x slower) 774.91 ns |
27
+ #
28
+ ####################################################################################################
29
+
30
+ require "bundler"
31
+ Bundler.require(:default, :benchmark)
32
+
33
+ require "type_toolkit"
34
+
35
+ module TypeKitDemo
36
+ # Provides the concrete implementation of `m`
37
+ class Parent
38
+ def m1 = "Parent#m1"
39
+ end
40
+
41
+ module I
42
+ interface!
43
+
44
+ abstract def m1; end
45
+ abstract def m2; end
46
+ abstract def not_implemented; end
47
+ end
48
+
49
+ # Inherits the concrete implementation of `m` from DemoParentClass.
50
+ class Child < Parent
51
+ include I
52
+
53
+ def m2 = "Child#m2"
54
+ end
55
+ end
56
+
57
+ module SorbetRuntimeDemo
58
+ # Provides the concrete implementation of `m`
59
+ class Parent
60
+ def m1 = "Parent#m1"
61
+ end
62
+
63
+ module I
64
+ extend T::Sig
65
+ extend T::Helpers
66
+
67
+ interface!
68
+
69
+ sig { abstract.returns(String) }
70
+ def m1; end
71
+
72
+ sig { abstract.returns(String) }
73
+ def m2; end
74
+
75
+ sig { abstract.returns(String) }
76
+ def not_implemented; end
77
+ end
78
+
79
+ # Inherits the concrete implementation of `m` from DemoParentClass.
80
+ class Child < Parent
81
+ include I
82
+
83
+ def m2 = "Child#m2"
84
+ end
85
+ end
86
+
87
+ module ManualDelegationDemo
88
+ class Parent
89
+ def m1 = "Parent#m1"
90
+ end
91
+
92
+ module I
93
+ def m1 = defined?(super) ? super : raise
94
+ def m2 = defined?(super) ? super : raise
95
+ def not_implemented = defined?(super) ? super : raise
96
+ end
97
+
98
+ # Inherits the concrete implementation of `m` from DemoParentClass.
99
+ class Child < Parent
100
+ include I
101
+
102
+ def m2 = "Child#m2"
103
+ end
104
+ end
105
+
106
+ type_toolkit_object = TypeKitDemo::Child.new
107
+ manual_delegation_object = ManualDelegationDemo::Child.new
108
+ sorbet_runtime_object = SorbetRuntimeDemo::Child.new
109
+
110
+ [:interpreter, :yjit].each do |mode|
111
+ if mode == :yjit
112
+ puts <<~MSG
113
+
114
+
115
+ ================================================================================
116
+ Enabling YJIT...
117
+ ================================================================================
118
+
119
+
120
+ MSG
121
+ RubyVM::YJIT.enable
122
+ end
123
+
124
+ warmup = 5
125
+ time = 10
126
+
127
+ width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length
128
+
129
+ puts "Benchmark the performance of calling the concrete implementation directly..."
130
+ Benchmark.ips do |x|
131
+ x.config(warmup:, time:)
132
+
133
+ x.report("type_toolkit".rjust(width)) do |times|
134
+ i = 0
135
+ while (i += 1) < times
136
+ type_toolkit_object.m2
137
+ end
138
+ end
139
+
140
+ x.report("sorbet-runtime".rjust(width)) do |times|
141
+ i = 0
142
+ while (i += 1) < times
143
+ sorbet_runtime_object.m2
144
+ end
145
+ end
146
+
147
+ x.report("manual delegation".rjust(width)) do |times|
148
+ i = 0
149
+ while (i += 1) < times
150
+ manual_delegation_object.m2
151
+ end
152
+ end
153
+
154
+ x.compare!
155
+ end
156
+
157
+ puts "\n\nBenchmark the performance of calling the inherited concrete implementation..."
158
+ Benchmark.ips do |x|
159
+ x.config(warmup:, time:)
160
+
161
+ x.report("type_toolkit".rjust(width)) do |times|
162
+ i = 0
163
+ while (i += 1) < times
164
+ type_toolkit_object.m1
165
+ end
166
+ end
167
+
168
+ x.report("sorbet-runtime".rjust(width)) do |times|
169
+ i = 0
170
+ while (i += 1) < times
171
+ sorbet_runtime_object.m1
172
+ end
173
+ end
174
+
175
+ x.report("manual delegation".rjust(width)) do |times|
176
+ i = 0
177
+ while (i += 1) < times
178
+ manual_delegation_object.m1
179
+ end
180
+ end
181
+
182
+ x.compare!
183
+ end
184
+
185
+ puts "\n\nTest the performance of calling an unimplemented abstract method..."
186
+ Benchmark.ips do |x|
187
+ x.config(warmup:, time:)
188
+
189
+ x.report("type_toolkit".rjust(width)) do |times|
190
+ i = 0
191
+ while (i += 1) < times
192
+ begin
193
+ type_toolkit_object.not_implemented
194
+ rescue AbstractMethodNotImplementedError # rubocop:disable Lint/SuppressedException
195
+ end
196
+ end
197
+ end
198
+
199
+ x.report("sorbet-runtime".rjust(width)) do |times|
200
+ i = 0
201
+ while (i += 1) < times
202
+ begin
203
+ sorbet_runtime_object.not_implemented
204
+ rescue NotImplementedError # rubocop:disable Lint/SuppressedException
205
+ end
206
+ end
207
+ end
208
+
209
+ x.report("manual delegation".rjust(width)) do |times|
210
+ i = 0
211
+ while (i += 1) < times
212
+ begin
213
+ manual_delegation_object.not_implemented
214
+ rescue StandardError # rubocop:disable Lint/SuppressedException
215
+ end
216
+ end
217
+ end
218
+
219
+ x.compare!
220
+ end
221
+ end
@@ -0,0 +1,128 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark the startup performance of declaring modules/interfaces in 3 different styles:
5
+ # - TypeToolkit (abstract gem)
6
+ # - Sorbet runtime
7
+ # - Manual delegation (defined?(super) pattern)
8
+
9
+ ############################################# Results #############################################
10
+ #
11
+ # ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
12
+ #
13
+ # | | Interpreter | YJIT |
14
+ # |-------------------|-------------------------:|---------------------------:|
15
+ # | sorbet-runtime | (21.34x slower) 50.79 μs | (152.34x slower) 377.27 μs |
16
+ # | type_toolkit | (4.18x slower) 9.95 μs | (4.18x slower) 10.35 μs |
17
+ # | manual delegation | 2.38 μs | 2.48 μs |
18
+ #
19
+ ####################################################################################################
20
+
21
+ require "bundler"
22
+ Bundler.require(:default, :benchmark)
23
+
24
+ require "type_toolkit"
25
+
26
+ warmup = 5
27
+ time = 10
28
+
29
+ width = ["type_toolkit", "sorbet-runtime", "manual delegation"].max_by(&:length).length
30
+
31
+ puts "Benchmark the time to declare an interface module with abstract methods..."
32
+
33
+ [:interpreter, :yjit].each do |mode|
34
+ if mode == :yjit
35
+ puts <<~MSG
36
+
37
+
38
+ ================================================================================
39
+ Enabling YJIT...
40
+ ================================================================================
41
+
42
+
43
+ MSG
44
+ RubyVM::YJIT.enable
45
+ end
46
+
47
+ Benchmark.ips do |x|
48
+ x.config(warmup:, time:)
49
+
50
+ x.report("type_toolkit".rjust(width)) do |times|
51
+ i = 0
52
+ while (i += 1) < times
53
+ interface = Module.new do
54
+ interface!
55
+
56
+ abstract def m1; end
57
+ abstract def m2; end
58
+ abstract def m3; end
59
+ end
60
+
61
+ Class.new do
62
+ include interface
63
+
64
+ def m1 = "m1"
65
+ def m2 = "m2"
66
+ def m3 = "m3"
67
+ end
68
+ end
69
+ end
70
+
71
+ x.report("sorbet-runtime".rjust(width)) do |times|
72
+ i = 0
73
+ while (i += 1) < times
74
+ interface = Module.new do
75
+ extend T::Sig
76
+ extend T::Helpers
77
+
78
+ interface!
79
+
80
+ sig { abstract.returns(String) }
81
+ def m1; end
82
+
83
+ sig { abstract.returns(String) }
84
+ def m2; end
85
+
86
+ sig { abstract.returns(String) }
87
+ def m3; end
88
+ end
89
+
90
+ Class.new do
91
+ extend T::Sig
92
+
93
+ include interface
94
+
95
+ sig { override.returns(String) }
96
+ def m1 = "m1"
97
+
98
+ sig { override.returns(String) }
99
+ def m2 = "m2"
100
+
101
+ sig { override.returns(String) }
102
+ def m3 = "m3"
103
+ end
104
+ end
105
+ end
106
+
107
+ x.report("manual delegation".rjust(width)) do |times|
108
+ i = 0
109
+ while (i += 1) < times
110
+ interface = Module.new do
111
+ def m1 = defined?(super) ? super : raise
112
+ def m2 = defined?(super) ? super : raise
113
+ def m3 = defined?(super) ? super : raise
114
+ end
115
+
116
+ Class.new do
117
+ include interface
118
+
119
+ def m1 = "m1"
120
+ def m2 = "m2"
121
+ def m3 = "m3"
122
+ end
123
+ end
124
+ end
125
+
126
+ x.compare!
127
+ end
128
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Benchmark if it's worth checking `Module.include?` before calling`Module.include`
4
+ # ... spoiler: meh, not really.
5
+
6
+ ############################################# Results #############################################
7
+ #
8
+ # ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin23]
9
+ #
10
+ # | | Interpreter | YJIT |
11
+ # |-------------------:|------------------------:|------------------------:|
12
+ # | just include | (2.39x slower) 54.35 ns | (8.53x slower) 35.29 ns |
13
+ # | check then include | 22.72 ns | 4.14 ns |
14
+ #
15
+ ####################################################################################################
16
+
17
+ require "bundler"
18
+ Bundler.require(:default, :benchmark)
19
+
20
+ module M1; end
21
+ module M2; end
22
+ class C1; end
23
+ class C2; end
24
+
25
+ [:interpreter, :yjit].each do |mode|
26
+ if mode == :yjit
27
+ puts <<~MSG
28
+
29
+
30
+ ================================================================================
31
+ Enabling YJIT...
32
+ ================================================================================
33
+
34
+
35
+ MSG
36
+ RubyVM::YJIT.enable
37
+ end
38
+
39
+ warmup = 5
40
+ time = 10
41
+
42
+ puts "Benchmark the performance of calling the concrete implementation directly..."
43
+ Benchmark.ips do |x|
44
+ x.config(warmup:, time:)
45
+
46
+ x.report("check then include") do |times|
47
+ i = 0
48
+ while (i += 1) < times
49
+ C1.include(M1) unless C1.include?(M1)
50
+ end
51
+ end
52
+
53
+ x.report("just include") do |times|
54
+ i = 0
55
+ while (i += 1) < times
56
+ C2.include(M2)
57
+ end
58
+ end
59
+
60
+ x.compare!
61
+ end
62
+ end
data/config/default.yml CHANGED
@@ -2,3 +2,8 @@ TypeToolkit/DontExpectUnexpectedNil:
2
2
  Description: "Detects misuse of UnexpectedNilError (rescuing, raising, or asserting it)."
3
3
  Enabled: true
4
4
  VersionAdded: "0.1.0"
5
+
6
+ TypeToolkit/PreferNotNil:
7
+ Description: "Replaces T.must assertions with Type Toolkit's not_nil! assertion."
8
+ Enabled: true
9
+ VersionAdded: "0.1.0"
@@ -0,0 +1,119 @@
1
+ # The inherited implementation problem
2
+
3
+ Take this example:
4
+
5
+ ```ruby
6
+ class Parent
7
+ def m = "Parent#m"
8
+ end
9
+
10
+ module I
11
+ interface!
12
+
13
+ abstract def m = raise "Abstract method `#m` not implemented"
14
+ end
15
+
16
+ class Child < Parent
17
+ include I
18
+ end
19
+
20
+ Child.new.m
21
+ # => Vanilla Ruby: raises (from `I#m`)
22
+ # => With this gem: `Parent#m`
23
+ ```
24
+
25
+ There are two challenges here:
26
+
27
+ 1. We need to not let the `I#m` stub implementation "get in the way", so that we can find real implementation that get inherited from further ancestors (like `Parent#m`).
28
+ 2. *but* we still want something to raise an error if you attempt to call an unimplemented abstract method.
29
+
30
+ If you lookup `m` on an instance of `Child`, you would usually hit the empty stub `I#m` instead of the inherited implementation `Parent#m`:
31
+
32
+ ```ruby
33
+ Child.ancestors
34
+
35
+ # => [Child, Interface, Parent, Object, Kernel, BasicObject]
36
+ # ^ ^ ^
37
+ # | | Provides the implementation for Child to inherit
38
+ # | Its stub abstract method "gets in the way" and needs to be side-stepped
39
+ # We want Child to inherit the implementation from Parent
40
+
41
+
42
+ Child.instance_method(:m).owner
43
+ # => Vanilla Ruby: `I`
44
+ # => With this gem: `Parent`
45
+ ```
46
+
47
+ # Solution
48
+
49
+ Solve the inheritance problem by just yoinking the abstract method stub out of the ancestor chain, by just using `remove_method()`.
50
+
51
+ Now if you send `#m` to a child, there's no `I#m` implementation to hit, so it just jumps over to `Parent#m`. This is a direct method call with absolutely no runtime overhead.
52
+
53
+ This introduces a new problem, that now calling unimplemented abstract methods just raises `NoMethodError`, as if the name never existed. For a better developer experience, we'd like to give a more helpful message.
54
+
55
+ To do this, we implement `#method_missing` and check if the missing method name is one of the abstract methods (based on a list we append to every time you call `abstract`). If it is, we can raise our nicer error, otherwise we just delegate up the rest of the `#method_missing` chain (ultimately triggering a `NoMethodError`, like normal). This part isn't strictly necessary, but it's a nice-to-have. We can make it configurable.
56
+
57
+ Syntax:
58
+
59
+ ```ruby
60
+ module I
61
+ interface!
62
+
63
+ abstract def m; end
64
+ end
65
+ ```
66
+
67
+ That's it. That simple!
68
+
69
+ Pros:
70
+ - Good DX when calling an abstract method that you forgot to implement
71
+ - Really nice syntax
72
+
73
+ Cons:
74
+ - Some implementation complexity (but all tucked into this gem, with less than 130 lines of implementation code)
75
+ - Calls to implemented abstract methods are faster, at the expense of unimplemented ones
76
+ - 1.9x slower than the hand-written alternative
77
+ - 1.1x slower than sorbet-runtime alternative
78
+ - but that's totally acceptable, because this should never happen in a completed program. The performance of calls to actually implemented methods is what matters, since that's what the real code will be doing at a huge volume.
79
+
80
+ # Alternative 1: Hand-written delegation
81
+
82
+ You duplicate this delegation logic in every abstract method:
83
+
84
+ ```ruby
85
+ module I
86
+ interface!
87
+
88
+ # @abstract
89
+ def m = defined?(super) ? super : raise("Abstract method `#m` not implemented")
90
+ end
91
+ ```
92
+
93
+ Pros:
94
+ - There's no magic, no runtime needed.
95
+
96
+ Cons:
97
+ - Repetitive
98
+ - Can be forgotten
99
+ - There's more overhead on every method call (but only to abstract methods with an inherited implementation):
100
+ - Checking `defined?(super)`
101
+ - Making the `super` call
102
+ - It adds an extra frame to your backtrace which will be seen in debuggers and exception backtraces (unless you configure them to filter it out)
103
+
104
+
105
+ # Alternative 2: Sorbet runtime's solution
106
+
107
+ Here's a Sorbet version of the example: [Sorbet.run](https://sorbet.run/#%23%20typed%3A%20true%0A%0Aclass%20Parent%0A%20%20extend%20T%3A%3ASig%0A%0A%20%20sig%20%7B%20returns%28String%29%20%7D%0A%20%20def%20m%20%3D%20%22Parent%23m%22%0Aend%0A%20%20%0Amodule%20I%0A%20%20extend%20T%3A%3ASig%0A%20%20extend%20T%3A%3AHelpers%0A%0A%20%20interface!%0A%0A%20%20sig%20%7B%20abstract.returns%28String%29%20%7D%0A%20%20def%20m%3B%20end%0Aend%0A%0Aclass%20Child%20%3C%20Parent%3B%20end).
108
+
109
+ Sorbet runtime basically automates the hand-written solution above, by wrapping abstract methods:
110
+
111
+ https://github.com/sorbet/sorbet/blob/703498a0dcddbe7ec4b87ec6cc5d7d55cfa9b270/gems/sorbet-runtime/lib/types/private/methods/call_validation.rb#L48-L68
112
+
113
+ Pros:
114
+ - "just works"
115
+
116
+ Cons:
117
+ - All the downsides of doing this the hand-written way.
118
+ - To determine if a method is abstract or not, every `sig` needs to have its block evaluated, to see if it calls `abstract`.
119
+ - This used to be slower because it was defined via `defined_methods` with a block body. This produces a slower kind of method (`VM_METHOD_TYPE_BMETHOD`), than the equivalent code via `def` (`VM_METHOD_TYPE_ISEQ`). This was fixed in [this PR](https://github.com/sorbet/sorbet/pull/8238), which switch to using `module_eval` to define the method via `def`.
@@ -0,0 +1,79 @@
1
+ # The `def self.` problem
2
+
3
+ Ruby method definitions evaluate to the name of the method that was defined. This fact is used by the `public`/`protected`/`private` methods:
4
+
5
+ ```ruby
6
+ class
7
+ puts def demo; end
8
+ # Equivalent to `puts(:demo)`
9
+ end
10
+ ```
11
+
12
+ However, there's no way to distinguish whether the method was an instance method, or a singleton method:
13
+
14
+ ```ruby
15
+ class C
16
+ puts def demo; end # prints ":demo"
17
+ puts def self.demo; end # *also* prints ":demo"
18
+ end
19
+ ```
20
+
21
+ # Solution
22
+
23
+ Track into whether the last method call was an instance method or a singleton method, by hooking into `method_added` and `singleton_method_added`. See the `MethodDefRecorder` for details.
24
+
25
+ This way, both of these "just work"
26
+
27
+ ```rb
28
+ class C
29
+ # Correctly defines an abstract instance method
30
+ abstract def demo; end
31
+
32
+ # Correctly defines an abstract "class method"
33
+ abstract def self.demo; end
34
+ end
35
+ ```
36
+
37
+ # Alternative 1: Do nothing
38
+
39
+ This is already a problem for access level modifiers, which don't do anything to handle it:
40
+
41
+ ```ruby
42
+ class C
43
+ private def self.demo; end
44
+ # => ❌ undefined method 'demo' for class 'C' (NameError)
45
+ end
46
+ ```
47
+
48
+ We can just follow suit. However, there's a risk that if an instance method called `demo` _actually_ existed, we inadvertently make it abstract without intending. Again, the access level modifier methods already have this issue, but it's a sharp edge that we don't need to have.
49
+
50
+ If we choose to do nothing, we could encourage users to enable the [`Style/ClassMethodsDefinitions`](https://docs.rubocop.org/rubocop/cops_style.html#styleclassmethodsdefinitions) on the `EnforcedStyle: self_class` mode, so that their code bases don't contain `def self.foo` methods at all.
51
+
52
+ Pros:
53
+ - Simpler implementation (none!)
54
+
55
+ Cons:
56
+ - Sharp edge
57
+ - More complex mental model for users of RBS
58
+
59
+ # Alternative 2: separate macro
60
+
61
+ E.g.
62
+
63
+ ```rb
64
+ class C
65
+ # Correctly defines an abstract instance method
66
+ abstract_instance_method def demo; end
67
+
68
+ # Correctly defines an abstract "class method"
69
+ abstract_class_method def self.demo; end
70
+ end
71
+ ```
72
+
73
+ Pros:
74
+ - Simple implementation
75
+
76
+ Cons:
77
+ - Correct usage can't be enforced at runtime.
78
+ - Rubocop cop could enforce it, but that would need to be written
79
+ - Still a sharp edge, has the same cognitive complexity as alternative 1.
@@ -13,7 +13,7 @@ module RuboCop
13
13
  name: "rubocop-type_toolkit",
14
14
  version: ::TypeToolkit::VERSION,
15
15
  homepage: "https://github.com/Shopify/type_toolkit",
16
- description: "Detects misuse of UnexpectedNilError.",
16
+ description: "RuboCop rules for Type Toolkit.",
17
17
  )
18
18
  end
19
19