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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a743f2b4d2786fb8c08c666974f6f913a4baea59dc9d99dba7a5982ec13441a
4
- data.tar.gz: '097ea19c5691a4416de13f1d49ad05544793a9246cd358fcc5619ccca39f6419'
3
+ metadata.gz: 12374ec2c61c2218bb47b3e2f79d1d7c9bf59a7f4d75d2c429ebeecb02d4d51d
4
+ data.tar.gz: 13ef655859f7305f5ce532d6f9a3ac53513f2fcc8935d1b1c1f85b07c5f9e5d9
5
5
  SHA512:
6
- metadata.gz: 66a3f796e922912c0909a4a7548fd72f75a2cee65800a64c37189176cb6bbb323e4693c458b6bef99f828550e83693b036ef3f1e46542a4cf8da547255ed7c76
7
- data.tar.gz: 846e557b1e223a17adfc8ad6df338a9032cc3ea983e8f0079881f85f9012b0f20968ffd08f176386504393133cc63124e19a0cdb88b6124bac5452f0f20798b3
6
+ metadata.gz: 1ef3a56d5b8838ffcd97adf208f2ce67598dfa6157414ba084a91e146d43669ab1b06810659c9b3226ad5cd80f2ac875f937301238f45ba68d918ddabd177fd4
7
+ data.tar.gz: df81fdc2a72a0146a244a9a1028c227d789db201d3e1f66300a1cb4ccc9caf5345b27474dac8fe858a6f5753ae20bae184380a4837afd1c45da8a9ec4de1f6cf
@@ -19,12 +19,8 @@ jobs:
19
19
  rubygems: 4.0.4
20
20
  - name: Run type check
21
21
  run: bundle exec rake typecheck
22
- - name: Run type check with Prism parser
23
- run: bundle exec rake typecheck_prism
24
- continue-on-error: true
25
22
  - name: Run tests
26
23
  run: bundle exec rake test
27
- continue-on-error: true
28
24
  - name: Lint Ruby files
29
25
  run: bundle exec rake rubocop
30
26
  - name: Verify gem RBIs are up-to-date
data/README.md CHANGED
@@ -70,6 +70,67 @@ last_delivery = user.not_nil!
70
70
  .deliveries.last.not_nil!
71
71
  ```
72
72
 
73
+ The `TypeToolkit/PreferNotNil` RuboCop cop autocorrects `T.must` and `T.must_because` to `.not_nil!`.
74
+ For `T.must_because`, the reason block's source is preserved as comments inside a parenthesized expression,
75
+ so the correction also works in chained calls and nested expressions:
76
+
77
+ ```rb
78
+ # Before
79
+ value = T.must_because(foo) { "Proven non-nil by validation." }
80
+
81
+ # After
82
+ value = (
83
+ # Proven non-nil by validation.
84
+ foo.not_nil!
85
+ )
86
+ ```
87
+
88
+ The reason is documentation only after correction: it is no longer evaluated or included in the exception message.
89
+ Calls with heredocs in the value or reason block are reported but not autocorrected, because heredoc bodies can extend beyond the block's source range.
90
+
91
+ ### Interfaces
92
+
93
+ 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.
94
+
95
+ The Type Toolkit provides runtime support for interfaces (marked with `interface!`) and abstract methods (marked with `abstract` before the `def` keyword).
96
+
97
+ Example:
98
+
99
+ ```ruby
100
+ module Notifier
101
+ interface!
102
+
103
+ #: (String) -> void
104
+ abstract def send_notification(message); end
105
+ end
106
+
107
+ class SlackNotifier
108
+ include Notifier
109
+
110
+ # @override
111
+ #: (String) -> void
112
+ def send_notification(message)
113
+ puts "Posting to Slack API: #{message.inspect}"
114
+ end
115
+ end
116
+
117
+ SlackNotifier.new.send_notification("Hello, world!") # ✅
118
+ # => Posting to Slack API: "Hello, world!"
119
+ ```
120
+
121
+ Unimplemented abstract methods cannot be called, and the Type Toolkit runtime will raise an error if you try to do so:
122
+
123
+ ```ruby
124
+ class EmailNotifier
125
+ include Notifier
126
+
127
+ # Oops, forgot to implement `#send_notification`!
128
+ end
129
+
130
+ EmailNotifier.new.send_notification("Hello, world!") # ❌ TypeToolkit::AbstractMethodNotImplementedError
131
+ # => Abstract method #send_notification was never implemented.
132
+ ```
133
+
73
134
  ## Guiding Principles
74
135
 
75
136
  ### Blazingly fast™
data/Rakefile CHANGED
@@ -14,13 +14,6 @@ desc "alias for typecheck"; task tc: :typecheck
14
14
  desc "alias for typecheck"; task srb: :typecheck
15
15
  desc "alias for typecheck"; task sorbet: :typecheck
16
16
 
17
- desc "Type-check the code base with Sorbet using Prism"
18
- task :typecheck_prism do
19
- sh "bundle exec srb tc --parser=prism" do |ok, _res|
20
- abort unless ok
21
- end
22
- end
23
-
24
17
  require "minitest/test_task"
25
18
 
26
19
  Minitest::TestTask.create do |t|
@@ -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 and T.must_because assertions with Type Toolkit's not_nil! assertion."
8
+ Enabled: true
9
+ VersionAdded: "0.1.0"