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,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # A shim module representing (a subset of) the common interface between Method and UnboundMethod.
5
+ module MethodOrUnboundMethod
6
+ sig { returns(T::Module[T.untyped]) }
7
+ def owner; end
8
+
9
+ sig { returns(Symbol) }
10
+ def name; end
11
+ end
12
+
13
+ class Method
14
+ include MethodOrUnboundMethod
15
+ end
16
+
17
+ class UnboundMethod
18
+ include MethodOrUnboundMethod
19
+ end
@@ -35,9 +35,12 @@ module Minitest
35
35
  def it(desc = T.unsafe(nil), &block); end
36
36
 
37
37
  sig do
38
- params(block: T.proc.bind(T.attached_class).void).void
38
+ params(
39
+ type: T.anything,
40
+ block: T.proc.bind(T.attached_class).void,
41
+ ).void
39
42
  end
40
- def before(&block); end
43
+ def before(type = T.unsafe(nil), &block); end
41
44
  end
42
45
  end
43
46
  end
@@ -4,14 +4,17 @@
4
4
  module RuboCop
5
5
  module Minitest
6
6
  module AssertOffense
7
- sig { params(source: String).void }
8
- def assert_offense(source); end
7
+ sig { params(source: String, file: T.nilable(String), replacements: String).void }
8
+ def assert_offense(source, file = nil, **replacements); end
9
9
 
10
10
  sig { params(source: String).void }
11
11
  def assert_no_offenses(source); end
12
12
 
13
13
  sig { params(source: String).void }
14
14
  def assert_correction(source); end
15
+
16
+ sig { void }
17
+ def assert_no_corrections; end
15
18
  end
16
19
  end
17
20
  end
@@ -0,0 +1,419 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ require "spec_helper"
5
+
6
+ module TypeToolkit
7
+ class InterfaceSpec < Minitest::Spec
8
+ module SimpleInterface
9
+ interface!
10
+
11
+ abstract def m1 = assert_never_called!
12
+ abstract def m2 = assert_never_called!
13
+ end
14
+
15
+ # A normal class that's completely unrelated `SimpleInterface`.
16
+ class NonImpl
17
+ def m1 = "NonImpl#m1"
18
+ # Does not implement `m2`
19
+ end
20
+
21
+ # A class that implements some but not all of the SimpleInterface's abstract methods.
22
+ class PartialImpl
23
+ include SimpleInterface
24
+
25
+ def m1 = "PartialImpl#m1"
26
+ # Does not provide an implementation for `m2`
27
+ end
28
+
29
+ # A class that implements all of the SimpleInterface's abstract methods.
30
+ class FullImpl
31
+ include SimpleInterface
32
+
33
+ def initialize
34
+ @a = 1
35
+ @b = 2
36
+ end
37
+
38
+ def m1 = "FullImpl#m1"
39
+ def m2 = "FullImpl#m2"
40
+ end
41
+
42
+ # A class that provides a partial implementation of `SimpleInterface` for `PartiallyInheritsItsImpl`.
43
+ class PartialParent
44
+ def m1 = "PartialParent#m1"
45
+ # Does not implement `m2`
46
+ end
47
+
48
+ # A class that implements all of the SimpleInterface's abstract methods, some via inheritance, and some via direct implementation.
49
+ class PartiallyInheritsItsImpl < PartialParent
50
+ include SimpleInterface
51
+
52
+ def m2 = "PartiallyInheritsItsImpl#m2"
53
+ end
54
+
55
+ describe "An interface" do
56
+ describe ".abstract macro" do
57
+ it "returns the method name" do
58
+ test_context = self
59
+
60
+ Module.new do
61
+ interface!
62
+
63
+ test_context.assert_equal(:the_method_name, abstract(def the_method_name; end))
64
+ end
65
+ end
66
+
67
+ it "raises when it sees a different method name than what MethodDefRecorder recorded" do
68
+ error = assert_raises(RuntimeError) do
69
+ Module.new do
70
+ interface!
71
+
72
+ # Simulate a conflicting `method_added` call by sneaking in a method definition
73
+ # between the `def` and the `abstract` call.
74
+ def m1; end
75
+
76
+ def something_else; end
77
+
78
+ abstract(:m1)
79
+ end
80
+ end
81
+
82
+ # Do not rely on this message content! Its content is subject to change!
83
+ assert_equal <<~EXPECTED.chomp, error.message
84
+ `abstract` expected to see `#m1`, but the last recorded method was called `something_else`.
85
+ This can happen when `abstract` is combined with other metaprogramming.
86
+ If you think this is a bug, please open an issue: https://github.com/Shopify/type_toolkit/issues
87
+ EXPECTED
88
+ end
89
+ end
90
+
91
+ describe ".declared_abstract_instance_methods" do
92
+ it "contains all declared abstract methods" do
93
+ assert_equal [:m1, :m2], SimpleInterface.declared_abstract_instance_methods
94
+ assert_equal [:m1, :m2], SimpleInterface.declared_abstract_instance_methods(true)
95
+ assert_equal [:m1, :m2], SimpleInterface.declared_abstract_instance_methods(false)
96
+ end
97
+ end
98
+
99
+ describe ".abstract_instance_methods" do
100
+ it "only contains unimplemented abstract methods" do
101
+ assert_equal [:m1, :m2], SimpleInterface.abstract_instance_methods
102
+ assert_equal [:m1, :m2], SimpleInterface.abstract_instance_methods(true)
103
+ assert_equal [:m1, :m2], SimpleInterface.abstract_instance_methods(false)
104
+ end
105
+ end
106
+
107
+ describe ".abstract_method?" do
108
+ it "returns true for abstract methods" do
109
+ assert SimpleInterface.abstract_method?(:m1)
110
+ assert SimpleInterface.abstract_method?(:m2)
111
+ end
112
+
113
+ it "returns false for non-abstract methods" do
114
+ refute SimpleInterface.abstract_method?(:inspect)
115
+ end
116
+ end
117
+
118
+ describe ".abstract_method_declared?" do
119
+ it "returns true for abstract methods" do
120
+ assert SimpleInterface.abstract_method_declared?(:m1)
121
+ assert SimpleInterface.abstract_method_declared?(:m2)
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "A class that does not implement the interface" do
127
+ describe "a method with the same name as another interface's members" do
128
+ # These tests sanity-check that our runtime trickery didn't accidentally change the
129
+ # normal behaviour of method calling and `method_missing` for unrelated classes.
130
+
131
+ before do
132
+ @class = NonImpl
133
+ @x = NonImpl.new
134
+ end
135
+
136
+ it "#respond_to? returns true" do
137
+ assert_respond_to @x, :m1
138
+ end
139
+
140
+ it "can be called like normal" do
141
+ assert_equal "NonImpl#m1", @x.m1
142
+ end
143
+
144
+ describe "a Method object for it" do
145
+ it "can be called like normal" do
146
+ assert_equal "NonImpl#m1", @x.method(:m1).call
147
+ end
148
+
149
+ it "is not abstract" do
150
+ refute_predicate @x.method(:m1), :abstract?
151
+ end
152
+ end
153
+
154
+ describe "an UnboundMethod object for it" do
155
+ before { @um = @x.method(:m1).unbind }
156
+ it "can be called like normal" do
157
+ assert_equal "NonImpl#m1", @um.bind_call(@x)
158
+ end
159
+
160
+ it "is not abstract" do
161
+ refute_predicate @um, :abstract?
162
+ end
163
+ end
164
+
165
+ it "calls a defined method like normal" do
166
+ assert_respond_to @x, :m1
167
+ assert_equal "NonImpl#m1", @x.m1
168
+ assert_equal "NonImpl#m1", @x.method(:m1).call
169
+ refute_predicate @x.method(:m1), :abstract?
170
+ refute_predicate @x.method(:m1).unbind, :abstract?
171
+ end
172
+
173
+ it "raises NoMethodError for an undefined method like normal" do
174
+ refute_respond_to @x, :m2
175
+ assert_raises(NoMethodError) { @x.m2 }
176
+ end
177
+ end
178
+
179
+ it "does not respond to .abstract_method?" do
180
+ refute_respond_to @class, :abstract_method?
181
+ assert_raises(NoMethodError) { @class.abstract_method?(:m1) }
182
+ end
183
+
184
+ it "does not respond to .abstract_method_declared?" do
185
+ refute_respond_to @class, :abstract_method_declared?
186
+ assert_raises(NoMethodError) { @class.abstract_method_declared?(:m1) }
187
+ end
188
+
189
+ it "does not respond to .declared_abstract_instance_methods" do
190
+ refute_respond_to @class, :declared_abstract_instance_methods
191
+ assert_raises(NoMethodError) { @class.declared_abstract_instance_methods }
192
+ end
193
+
194
+ it "does not respond to .abstract_instance_methods" do
195
+ refute_respond_to @class, :abstract_instance_methods
196
+ assert_raises(NoMethodError) { @class.abstract_instance_methods }
197
+ end
198
+ end
199
+
200
+ describe "A class that partially implements the interface" do
201
+ before do
202
+ @class = PartialImpl
203
+ @x = PartialImpl.new
204
+ end
205
+
206
+ describe "calling an implemented abstract method" do
207
+ it "calls the concrete implementation" do
208
+ assert_respond_to @x, :m1
209
+ assert_equal "PartialImpl#m1", @x.m1
210
+ assert_equal "PartialImpl#m1", @x.method(:m1).call
211
+ refute_predicate @x.method(:m1), :abstract?
212
+ refute_predicate @x.method(:m1).unbind, :abstract?
213
+ end
214
+ end
215
+
216
+ describe "calling an unimplemented abstract method" do
217
+ it "raises AbstractMethodNotImplementedError" do
218
+ assert_respond_to @x, :m2
219
+
220
+ # Notice it's not `NoMethodError`, so we can give a better error message.
221
+ e = assert_abstract { @x.m2 }
222
+
223
+ # Do not rely on this message content! Its content is subject to change!
224
+ # We only test it to ensure it's formatted correctly.
225
+ assert_equal "Abstract method `#m2` was never implemented.", e.message
226
+
227
+ m2 = @x.method(:m2)
228
+ assert_kind_of Method, m2
229
+ assert_predicate m2, :abstract?
230
+ assert_predicate m2.unbind, :abstract?
231
+ end
232
+ end
233
+
234
+ describe "calling a non-abstract method" do
235
+ it "calls the concrete implementation" do
236
+ assert_respond_to @x, :inspect
237
+ assert_kind_of String, @x.inspect
238
+ assert_kind_of String, @x.method(:inspect).call
239
+ refute_predicate @x.method(:inspect), :abstract?
240
+ refute_predicate @x.method(:inspect).unbind, :abstract?
241
+ end
242
+ end
243
+
244
+ describe ".abstract_method?" do
245
+ it "returns false for abstract methods that have been implemented" do
246
+ refute @class.abstract_method?(:m1)
247
+ end
248
+
249
+ it "returns true for abstract methods that have not been implemented" do
250
+ assert @class.abstract_method?(:m2)
251
+ end
252
+
253
+ it "returns false for non-abstract methods" do
254
+ refute @class.abstract_method?(:inspect)
255
+ end
256
+ end
257
+
258
+ describe ".abstract_method_declared?" do
259
+ it "is true for all abstract methods" do
260
+ assert @class.abstract_method_declared?(:m1) # Even the one that's been implemented
261
+ assert @class.abstract_method_declared?(:m2)
262
+ end
263
+
264
+ it "is false for non-abstract methods" do
265
+ assert @class.public_method_defined?(:inspect) # precondition
266
+ refute @class.abstract_method_declared?(:inspect)
267
+ end
268
+ end
269
+
270
+ describe ".declared_abstract_instance_methods" do
271
+ it "returns all declared abstract methods, even those that have been implemented" do
272
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods
273
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods(true)
274
+ assert_equal [], @class.declared_abstract_instance_methods(false)
275
+ end
276
+ end
277
+
278
+ describe ".abstract_instance_methods" do
279
+ it "returns only unimplemented abstract methods" do
280
+ assert_equal [:m2], @class.abstract_instance_methods
281
+ assert_equal [:m2], @class.abstract_instance_methods(true)
282
+ assert_equal [], @class.abstract_instance_methods(false)
283
+ end
284
+ end
285
+ end
286
+
287
+ describe "A class that fully implements the interface" do
288
+ before do
289
+ @class = FullImpl
290
+ @x = FullImpl.new
291
+ end
292
+
293
+ describe "calling an implemented abstract method" do
294
+ it "calls the concrete implementation" do
295
+ assert_respond_to @x, :m1
296
+ assert_equal "FullImpl#m1", @x.m1
297
+ assert_equal "FullImpl#m1", @x.method(:m1).call
298
+ refute_predicate @x.method(:m1), :abstract?
299
+ refute_predicate @x.method(:m1).unbind, :abstract?
300
+
301
+ assert_respond_to @x, :m2
302
+ assert_equal "FullImpl#m2", @x.m2
303
+ assert_equal "FullImpl#m2", @x.method(:m2).call
304
+ refute_predicate @x.method(:m2), :abstract?
305
+ refute_predicate @x.method(:m2).unbind, :abstract?
306
+ end
307
+ end
308
+
309
+ describe ".abstract_method?" do
310
+ it "returns false for abstract methods that have been implemented" do
311
+ refute @class.abstract_method?(:m1)
312
+ refute @class.abstract_method?(:m2)
313
+ end
314
+
315
+ it "returns false for non-abstract methods" do
316
+ refute @class.abstract_method?(:inspect)
317
+ end
318
+ end
319
+
320
+ describe ".abstract_method_declared?" do
321
+ it "returns true for all abstract methods" do
322
+ assert @class.abstract_method_declared?(:m1)
323
+ assert @class.abstract_method_declared?(:m2)
324
+ end
325
+ end
326
+
327
+ describe ".declared_abstract_instance_methods" do
328
+ it "returns all declared abstract methods, even those that have been implemented" do
329
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods
330
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods(true)
331
+ assert_equal [], @class.declared_abstract_instance_methods(false)
332
+ end
333
+ end
334
+
335
+ describe ".abstract_instance_methods" do
336
+ it "returns only unimplemented abstract methods" do
337
+ assert_equal [], @class.abstract_instance_methods
338
+ assert_equal [], @class.abstract_instance_methods(true)
339
+ assert_equal [], @class.abstract_instance_methods(false)
340
+ end
341
+ end
342
+ end
343
+
344
+ describe "A class that fully implements the interface, partially via inheritance" do
345
+ before do
346
+ @class = PartiallyInheritsItsImpl
347
+ @x = PartiallyInheritsItsImpl.new
348
+ end
349
+
350
+ describe "calling an abstract method with an inherited implementation" do
351
+ it "calls the inherited implementation" do
352
+ assert_respond_to @x, :m1
353
+ assert_equal "PartialParent#m1", @x.m1
354
+ assert_equal "PartialParent#m1", @x.method(:m1).call
355
+ refute_predicate @x.method(:m1), :abstract?
356
+ refute_predicate @x.method(:m1).unbind, :abstract?
357
+ end
358
+ end
359
+
360
+ describe "calling an abstract method with defined in the child implementation" do
361
+ it "calls the child implementation" do
362
+ assert_respond_to @x, :m2
363
+ assert_equal "PartiallyInheritsItsImpl#m2", @x.m2
364
+ assert_equal "PartiallyInheritsItsImpl#m2", @x.method(:m2).call
365
+ refute_predicate @x.method(:m2), :abstract?
366
+ refute_predicate @x.method(:m2).unbind, :abstract?
367
+ end
368
+ end
369
+
370
+ describe ".abstract_method?" do
371
+ it "returns false for abstract methods that have been implemented" do
372
+ refute @class.abstract_method?(:m1)
373
+ refute @class.abstract_method?(:m2)
374
+ end
375
+
376
+ it "returns false for non-abstract methods" do
377
+ refute @class.abstract_method?(:inspect)
378
+ end
379
+ end
380
+
381
+ describe ".abstract_method_declared?" do
382
+ it "returns true for all abstract methods" do
383
+ assert @class.abstract_method_declared?(:m1)
384
+ assert @class.abstract_method_declared?(:m2)
385
+ end
386
+ end
387
+
388
+ describe ".declared_abstract_instance_methods" do
389
+ it "returns all declared abstract methods, even those that have been implemented" do
390
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods
391
+ assert_equal [:m1, :m2], @class.declared_abstract_instance_methods(true)
392
+ assert_equal [], @class.declared_abstract_instance_methods(false)
393
+ end
394
+ end
395
+
396
+ describe ".abstract_instance_methods" do
397
+ it "returns only unimplemented abstract methods" do
398
+ assert_equal [], @class.abstract_instance_methods
399
+ assert_equal [], @class.abstract_instance_methods(true)
400
+ assert_equal [], @class.abstract_instance_methods(false)
401
+ end
402
+ end
403
+ end
404
+
405
+ describe "Abstract class methods" do
406
+ it "are not yet supported" do
407
+ test_context = self
408
+
409
+ Module.new do
410
+ interface!
411
+
412
+ test_context.assert_raises(NotImplementedError) do
413
+ abstract def self.abstract_class_method; end # rubocop:disable Style/ClassMethodsDefinitions
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module TypeToolkit
6
+ module Misc
7
+ # This test just characterizes how `method_added` hooks work. It doesn't have anything to do with TypeToolkit itself,
8
+ # but it's useful for devs to confirm how this works across instance and class methods.
9
+ class HowMethodAddedHooksWorkSpec < Minitest::Spec
10
+ it "characterizes how method_added hooks work" do
11
+ test_context = self
12
+
13
+ Class.new do
14
+ class << self
15
+ attr_reader :__last_method_def
16
+
17
+ class << self
18
+ def __last_method_def
19
+ attached_object.__last_method_def
20
+ end
21
+ end
22
+
23
+ def method_added(m)
24
+ is_singleton_method = false
25
+ @__last_method_def = [m, is_singleton_method]
26
+
27
+ super
28
+ end
29
+
30
+ def singleton_method_added(m)
31
+ return super if m == :singleton_method_added
32
+
33
+ is_singleton_method = true
34
+ @__last_method_def = [m, is_singleton_method]
35
+
36
+ super
37
+ end
38
+ end
39
+
40
+ test_context.assert_nil __last_method_def
41
+
42
+ def instance_method; end
43
+
44
+ test_context.assert_equal [:instance_method, false], __last_method_def
45
+
46
+ def self.singleton_method_via_self_dot; end # rubocop:disable Style/ClassMethodsDefinitions
47
+
48
+ test_context.assert_equal [:singleton_method_via_self_dot, true], __last_method_def
49
+
50
+ r = class << self
51
+ def singleton_method_via_sclass; end
52
+
53
+ __last_method_def
54
+ end
55
+
56
+ test_context.assert_equal [:singleton_method_via_sclass, true], r
57
+
58
+ test_context.assert_equal [:singleton_method_via_sclass, true], __last_method_def
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end