type_toolkit 0.0.4 → 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 +45 -1
  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 +2 -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
@@ -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
@@ -0,0 +1,405 @@
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
+ end
405
+ end
@@ -0,0 +1,229 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "spec_helper"
5
+ require "rubocop"
6
+ require "rubocop/minitest/assert_offense"
7
+ require "rubocop-type_toolkit"
8
+
9
+ module RuboCop
10
+ module Cop
11
+ module TypeToolkit
12
+ class PreferNotNilSpec < ::Minitest::Spec
13
+ include RuboCop::Minitest::AssertOffense
14
+
15
+ MSG = "TypeToolkit/PreferNotNil: Use `.not_nil!` instead of `T.must()`."
16
+
17
+ before do
18
+ @cop = PreferNotNil.new
19
+ end
20
+
21
+ it "autocorrects T.must" do
22
+ assert_offense(<<~RUBY)
23
+ value = T.must(foo)
24
+ ^^^^^^^^^^^ #{MSG}
25
+ RUBY
26
+
27
+ assert_correction(<<~RUBY)
28
+ value = foo.not_nil!
29
+ RUBY
30
+ end
31
+
32
+ it "autocorrects ::T.must" do
33
+ assert_offense(<<~RUBY)
34
+ value = ::T.must(foo)
35
+ ^^^^^^^^^^^^^ #{MSG}
36
+ RUBY
37
+
38
+ assert_correction(<<~RUBY)
39
+ value = foo.not_nil!
40
+ RUBY
41
+ end
42
+
43
+ it "autocorrects inside string interpolation" do
44
+ assert_offense(<<~RUBY)
45
+ string = "\#{T.must(foo)}"
46
+ ^^^^^^^^^^^ #{MSG}
47
+ RUBY
48
+
49
+ assert_correction(<<~RUBY)
50
+ string = "\#{foo.not_nil!}"
51
+ RUBY
52
+ end
53
+
54
+ it "preserves the precedence of conditional and range expressions" do
55
+ assert_offense(<<~RUBY)
56
+ conditional = T.must(condition ? foo : bar)
57
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
58
+ range = T.must(foo..bar)
59
+ ^^^^^^^^^^^^^^^^ #{MSG}
60
+ RUBY
61
+
62
+ assert_correction(<<~RUBY)
63
+ conditional = (condition ? foo : bar).not_nil!
64
+ range = (foo..bar).not_nil!
65
+ RUBY
66
+ end
67
+
68
+ it "parenthesizes every expression that requires it" do
69
+ assert_offense(<<~RUBY)
70
+ operator = T.must(foo + bar)
71
+ ^^^^^^^^^^^^^^^^^ #{MSG}
72
+ logical = T.must(foo || bar)
73
+ ^^^^^^^^^^^^^^^^^^ #{MSG}
74
+ grouped = T.must((foo || bar))
75
+ ^^^^^^^^^^^^^^^^^^^^ #{MSG}
76
+ assignment = T.must(foo = bar)
77
+ ^^^^^^^^^^^^^^^^^ #{MSG}
78
+ block_value = T.must(foo { bar })
79
+ ^^^^^^^^^^^^^^^^^^^ #{MSG}
80
+ defined_value = T.must(defined?(foo))
81
+ ^^^^^^^^^^^^^^^^^^^^^ #{MSG}
82
+ def example
83
+ T.must(yield foo)
84
+ ^^^^^^^^^^^^^^^^^ #{MSG}
85
+ end
86
+ def implicit_super
87
+ T.must(super)
88
+ ^^^^^^^^^^^^^ #{MSG}
89
+ end
90
+ def explicit_super
91
+ T.must(super(foo))
92
+ ^^^^^^^^^^^^^^^^^^ #{MSG}
93
+ end
94
+ RUBY
95
+
96
+ assert_correction(<<~RUBY)
97
+ operator = (foo + bar).not_nil!
98
+ logical = (foo || bar).not_nil!
99
+ grouped = (foo || bar).not_nil!
100
+ assignment = (foo = bar).not_nil!
101
+ block_value = (foo { bar }).not_nil!
102
+ defined_value = (defined?(foo)).not_nil!
103
+ def example
104
+ (yield foo).not_nil!
105
+ end
106
+ def implicit_super
107
+ (super).not_nil!
108
+ end
109
+ def explicit_super
110
+ (super(foo)).not_nil!
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ it "preserves the precedence of command calls" do
116
+ assert_offense(<<~RUBY)
117
+ value = T.must(fetch value)
118
+ ^^^^^^^^^^^^^^^^^^^ #{MSG}
119
+ RUBY
120
+
121
+ assert_correction(<<~RUBY)
122
+ value = (fetch value).not_nil!
123
+ RUBY
124
+ end
125
+
126
+ it "does not add unnecessary parentheses to parenthesized calls" do
127
+ assert_offense(<<~RUBY)
128
+ value = T.must(fetch(value))
129
+ ^^^^^^^^^^^^^^^^^^^^ #{MSG}
130
+ RUBY
131
+
132
+ assert_correction(<<~RUBY)
133
+ value = fetch(value).not_nil!
134
+ RUBY
135
+ end
136
+
137
+ it "preserves comments in multiline calls" do
138
+ assert_offense(<<~RUBY)
139
+ value = T.must(
140
+ ^^^^^^^ #{MSG}
141
+ # Proven non-nil by validation.
142
+ foo,
143
+ )
144
+ RUBY
145
+
146
+ assert_correction(<<~RUBY)
147
+ value = (
148
+ # Proven non-nil by validation.
149
+ foo
150
+ ).not_nil!
151
+ RUBY
152
+ end
153
+
154
+ it "autocorrects multiline calls with whitespace before the method" do
155
+ assert_offense(<<~RUBY)
156
+ first = T .must(
157
+ ^^^^^^^^ #{MSG}
158
+ foo,
159
+ )
160
+ second = T
161
+ ^ #{MSG}
162
+ .must(
163
+ bar,
164
+ )
165
+ RUBY
166
+
167
+ assert_correction(<<~RUBY)
168
+ first = (
169
+ foo
170
+ ).not_nil!
171
+ second = (
172
+ bar
173
+ ).not_nil!
174
+ RUBY
175
+ end
176
+
177
+ it "removes a trailing comma before an inline comment" do
178
+ assert_offense(<<~RUBY)
179
+ value = T.must(
180
+ ^^^^^^^ #{MSG}
181
+ foo, # Proven non-nil.
182
+ )
183
+ RUBY
184
+
185
+ assert_correction(<<~RUBY)
186
+ value = (
187
+ foo # Proven non-nil.
188
+ ).not_nil!
189
+ RUBY
190
+ end
191
+
192
+ it "autocorrects nested T.must calls" do
193
+ assert_offense(<<~RUBY)
194
+ value = T.must(T.must(foo).bar)
195
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
196
+ ^^^^^^^^^^^ #{MSG}
197
+ RUBY
198
+
199
+ assert_correction(<<~RUBY)
200
+ value = foo.not_nil!.bar.not_nil!
201
+ RUBY
202
+ end
203
+
204
+ it "autocorrects three nested T.must calls" do
205
+ assert_offense(<<~RUBY)
206
+ value = T.must(T.must(T.must(foo).bar).baz)
207
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
208
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
209
+ ^^^^^^^^^^^ #{MSG}
210
+ RUBY
211
+
212
+ assert_correction(<<~RUBY)
213
+ value = foo.not_nil!.bar.not_nil!.baz.not_nil!
214
+ RUBY
215
+ end
216
+
217
+ it "ignores other receivers, methods, and argument counts" do
218
+ assert_no_offenses(<<~RUBY)
219
+ Other::T.must(foo)
220
+ object.must(foo)
221
+ T.let(foo, String)
222
+ T.must(foo, bar)
223
+ T.must(*values)
224
+ RUBY
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
data/spec/spec_helper.rb CHANGED
@@ -6,3 +6,17 @@ require "type_toolkit"
6
6
 
7
7
  require "minitest/autorun"
8
8
  require "minitest/spec"
9
+
10
+ module Minitest
11
+ class Spec
12
+ #: () { () -> void } -> TypeToolkit::AbstractMethodNotImplementedError
13
+ def assert_abstract(&block)
14
+ assert_raises(TypeToolkit::AbstractMethodNotImplementedError, &block)
15
+ end
16
+ end
17
+ end
18
+
19
+ #: -> bot
20
+ def assert_never_called!
21
+ raise Minitest::Assertion, "This should never be called"
22
+ end