type_toolkit 0.0.6 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -4
- data/README.md +18 -0
- data/Rakefile +0 -7
- data/config/default.yml +1 -1
- data/docs/design/inherited_implementation_problem.md +141 -2
- data/lib/rubocop/cop/type_toolkit/prefer_not_nil.rb +69 -15
- data/lib/type_toolkit/dsl.rb +3 -1
- data/lib/type_toolkit/version.rb +1 -1
- data/sorbet/rbi/shims/rubocop_minitest.rbi +5 -2
- data/spec/interface_spec.rb +14 -0
- data/spec/misc/how_method_added_hooks_work_spec.rb +63 -0
- data/spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb +244 -9
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12374ec2c61c2218bb47b3e2f79d1d7c9bf59a7f4d75d2c429ebeecb02d4d51d
|
|
4
|
+
data.tar.gz: 13ef655859f7305f5ce532d6f9a3ac53513f2fcc8935d1b1c1f85b07c5f9e5d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ef3a56d5b8838ffcd97adf208f2ce67598dfa6157414ba084a91e146d43669ab1b06810659c9b3226ad5cd80f2ac875f937301238f45ba68d918ddabd177fd4
|
|
7
|
+
data.tar.gz: df81fdc2a72a0146a244a9a1028c227d789db201d3e1f66300a1cb4ccc9caf5345b27474dac8fe858a6f5753ae20bae184380a4837afd1c45da8a9ec4de1f6cf
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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,24 @@ 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
|
+
|
|
73
91
|
### Interfaces
|
|
74
92
|
|
|
75
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.
|
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|
|
data/config/default.yml
CHANGED
|
@@ -4,6 +4,6 @@ TypeToolkit/DontExpectUnexpectedNil:
|
|
|
4
4
|
VersionAdded: "0.1.0"
|
|
5
5
|
|
|
6
6
|
TypeToolkit/PreferNotNil:
|
|
7
|
-
Description: "Replaces T.must assertions with Type Toolkit's not_nil! assertion."
|
|
7
|
+
Description: "Replaces T.must and T.must_because assertions with Type Toolkit's not_nil! assertion."
|
|
8
8
|
Enabled: true
|
|
9
9
|
VersionAdded: "0.1.0"
|
|
@@ -6,7 +6,7 @@ Take this example:
|
|
|
6
6
|
class Parent
|
|
7
7
|
def m = "Parent#m"
|
|
8
8
|
end
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
module I
|
|
11
11
|
interface!
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ class Child < Parent
|
|
|
17
17
|
include I
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
Child.new.m
|
|
20
|
+
Child.new.m
|
|
21
21
|
# => Vanilla Ruby: raises (from `I#m`)
|
|
22
22
|
# => With this gem: `Parent#m`
|
|
23
23
|
```
|
|
@@ -117,3 +117,142 @@ Cons:
|
|
|
117
117
|
- All the downsides of doing this the hand-written way.
|
|
118
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
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`.
|
|
120
|
+
|
|
121
|
+
# Other languages
|
|
122
|
+
|
|
123
|
+
Most languages with abstract methods are ahead-of-time compiled. They don't have this issue because their compilers eagerly catch issues. Don't have the issue of runtime stub methods clobbering access to inherited implementation.
|
|
124
|
+
|
|
125
|
+
Python and Elixir are usual comparison points, as interpreted languages with growing static typing features.
|
|
126
|
+
|
|
127
|
+
## Python
|
|
128
|
+
|
|
129
|
+
Python has both [Abstract Base Classes](https://typing.python.org/en/latest/guides/libraries.html#abstract-classes-and-methods) and [Protocols](https://typing.python.org/en/latest/spec/protocol.html), and the two work differently.
|
|
130
|
+
|
|
131
|
+
Abstract classes don't have the inherited implementation problem, because they eagerly defend against instantiating a class with any unimplemented abstract methods. If you circumvent this defence, you just hit the `NotImplementedError` rather than an inherited implementation of the method.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
#!/usr/bin/env python3
|
|
135
|
+
|
|
136
|
+
from abc import ABC, abstractmethod
|
|
137
|
+
|
|
138
|
+
class Parent:
|
|
139
|
+
def m(self) -> str:
|
|
140
|
+
return "Parent.m"
|
|
141
|
+
|
|
142
|
+
class I(ABC):
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def m(self) -> str:
|
|
145
|
+
"""Subclasses must override"""
|
|
146
|
+
raise NotImplementedError()
|
|
147
|
+
|
|
148
|
+
class Child(I, Parent):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# print(Child.mro()) # Equivalent to Ruby's ancestor chain
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
Child() # Can't instantiate abstract class Child without an implementation for abstract method 'm'
|
|
155
|
+
except TypeError as e:
|
|
156
|
+
print(e)
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# Force the instantiation anyway
|
|
160
|
+
Child.__abstractmethods__ = frozenset()
|
|
161
|
+
Child().m() # => NotImplementedError
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
[Implicitly implementing a Protocol](https://typing.python.org/en/latest/spec/protocol.html#protocols:~:text=The%20default%20implementations%20cannot%20be%20used%20if%20the%20assignable%2Dto%20relationship%20is%20implicit%20and%20only%20structural%20%E2%80%93%20the%20semantics%20of%20inheritance%20is%20not%20changed%2E) doesn't have the problem, because the Protocol is not made part of the MRO (equivalent to Ruby's ancestor chain):
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
#!/usr/bin/env python3
|
|
168
|
+
|
|
169
|
+
from typing import Protocol
|
|
170
|
+
from abc import abstractmethod
|
|
171
|
+
|
|
172
|
+
class Parent:
|
|
173
|
+
def m(self) -> str:
|
|
174
|
+
return "Parent.m"
|
|
175
|
+
|
|
176
|
+
class MyProtocol(Protocol):
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def m(self) -> str:
|
|
179
|
+
raise NotImplementedError
|
|
180
|
+
|
|
181
|
+
class ImplicitChild(Parent): # <- MyProtocol not in this list
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Equivalent to Ruby's ancestor chain. `MyProtocol` is not in it.
|
|
185
|
+
print(ImplicitChild.mro()) # => [<class 'ImplicitChild'>, <class 'Parent'>, <class 'object'>]
|
|
186
|
+
|
|
187
|
+
# Calls the inherited implementation, no problem:
|
|
188
|
+
print(ImplicitChild().m()) # => "Parent.m"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
However, the moment you make the Protocol implementation explicit, you get the same problem as the Abstract Base Class case:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# ... continued from previous script
|
|
195
|
+
|
|
196
|
+
class ExplicitChild(MyProtocol, Parent):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
# Equivalent to Ruby's ancestor chain. `MyProtocol` is in it.
|
|
200
|
+
print(ExplicitChild.mro()) # => [<class 'ExplicitChild'>, <class 'MyProtocol'>, <class 'typing.Protocol'>, <class 'typing.Generic'>, <class 'Parent'>, <class 'object'>]
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
ExplicitChild() # Can't instantiate abstract class Child without an implementation for abstract method 'm'
|
|
204
|
+
except TypeError as e:
|
|
205
|
+
print(e)
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Force the instantiation anyway
|
|
209
|
+
ExplicitChild.__abstractmethods__ = frozenset()
|
|
210
|
+
ExplicitChild().m() # => NotImplementedError
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The [docs](https://typing.python.org/en/latest/spec/protocol.html#protocols:~:text=defined-,A,instantiated) are explicit about this:
|
|
214
|
+
|
|
215
|
+
> A class can explicitly inherit from multiple protocols and also from normal classes. In this case methods are resolved using normal MRO and a type checker verifies that all member assignability is correct. The semantics of `@abstractmethod` is not changed; all of them must be implemented by an explicit subclass before it can be instantiated.
|
|
216
|
+
|
|
217
|
+
## Elixir
|
|
218
|
+
|
|
219
|
+
Elixir doesn't have class-style inheritance. It's still interesting to see how it handles its equivalent to abstract methods: protocol functions.
|
|
220
|
+
When statically compiled, `elixirc` statically catches unimplemented methods and fails the build.
|
|
221
|
+
|
|
222
|
+
When dynamically executed (e.g. `iex`, `elixir`) will raise warnings for unimplemented methods. It records extra runtime metadata, used to surface more concrete error messages.
|
|
223
|
+
|
|
224
|
+
```elixir
|
|
225
|
+
#!/usr/bin/env elixir
|
|
226
|
+
|
|
227
|
+
defprotocol Size do
|
|
228
|
+
def size(value)
|
|
229
|
+
def empty?(value)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Protocol requirements are reified at runtime, can be reflected:
|
|
233
|
+
IO.inspect Size.__protocol__(:functions) # [empty?: 1, size: 1]
|
|
234
|
+
|
|
235
|
+
defimpl Size, for: List do
|
|
236
|
+
def size(value), do: length(value)
|
|
237
|
+
|
|
238
|
+
# Intentionally missing `def empty?`, Elixir warns:
|
|
239
|
+
# > warning: function empty?/1 required by protocol Size is not implemented
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Calling an implemented function
|
|
243
|
+
IO.inspect Size.size([]) # => 0
|
|
244
|
+
|
|
245
|
+
try do
|
|
246
|
+
Size.empty?([]) # Calling an unimplemented function
|
|
247
|
+
rescue
|
|
248
|
+
e in UndefinedFunctionError ->
|
|
249
|
+
# Replicates what e.g. `iex` would print.
|
|
250
|
+
{blame, _} = Exception.blame(:error, e, __STACKTRACE__)
|
|
251
|
+
IO.puts "(UndefinedFunctionError) #{blame.message}"
|
|
252
|
+
# => (UndefinedFunctionError) function Size.List.empty?/1 is undefined or private, but the behaviour Size expects it to be present
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Not how it knows that the `Size` behaviour expects it.
|
|
256
|
+
# Compare with the more generic error raised otherwise:
|
|
257
|
+
Size.foo # => (UndefinedFunctionError) function Size.foo/0 is undefined or private
|
|
258
|
+
```
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
module RuboCop
|
|
5
5
|
module Cop
|
|
6
6
|
module TypeToolkit
|
|
7
|
-
# Replaces Sorbet's `T.must
|
|
7
|
+
# Replaces Sorbet's `T.must` and `T.must_because` assertions with `.not_nil!`.
|
|
8
8
|
class PreferNotNil < Base
|
|
9
9
|
extend AutoCorrector
|
|
10
10
|
|
|
11
|
-
MSG = "Use `.not_nil!` instead of `T
|
|
12
|
-
RESTRICT_ON_SEND = [:must].freeze
|
|
11
|
+
MSG = "Use `.not_nil!` instead of `T.%<method>s()`."
|
|
12
|
+
RESTRICT_ON_SEND = [:must, :must_because].freeze
|
|
13
13
|
|
|
14
14
|
COMMA_BYTE = ",".ord
|
|
15
15
|
private_constant :COMMA_BYTE
|
|
@@ -19,16 +19,21 @@ module RuboCop
|
|
|
19
19
|
|
|
20
20
|
#: (RuboCop::AST::SendNode) -> void
|
|
21
21
|
def on_send(node)
|
|
22
|
-
return unless (argument =
|
|
22
|
+
return unless (argument = extract_assertion_argument(node))
|
|
23
23
|
|
|
24
|
-
if
|
|
25
|
-
|
|
24
|
+
block = node.block_node if node.method?(:must_because)
|
|
25
|
+
target = block || node
|
|
26
|
+
message = format(MSG, method: node.method_name)
|
|
27
|
+
|
|
28
|
+
if nested_assertion?(node) || (block && contains_heredoc?(block))
|
|
29
|
+
add_offense(target, message:)
|
|
26
30
|
else
|
|
27
31
|
replacement = replacement_for(argument)
|
|
28
32
|
correction = correction_for(node, argument, replacement)
|
|
33
|
+
correction = commented_correction(block, correction) if block
|
|
29
34
|
|
|
30
|
-
add_offense(
|
|
31
|
-
corrector.replace(
|
|
35
|
+
add_offense(target, message:) do |corrector|
|
|
36
|
+
corrector.replace(target, correction)
|
|
32
37
|
end
|
|
33
38
|
end
|
|
34
39
|
end
|
|
@@ -36,10 +41,10 @@ module RuboCop
|
|
|
36
41
|
private
|
|
37
42
|
|
|
38
43
|
#: (RuboCop::AST::SendNode) -> RuboCop::AST::Node?
|
|
39
|
-
def
|
|
44
|
+
def extract_assertion_argument(node)
|
|
40
45
|
receiver = node.receiver
|
|
41
46
|
return unless receiver.is_a?(RuboCop::AST::ConstNode)
|
|
42
|
-
return unless receiver.short_name == :T &&
|
|
47
|
+
return unless receiver.short_name == :T && RESTRICT_ON_SEND.include?(node.method_name) && node.arguments.one?
|
|
43
48
|
|
|
44
49
|
namespace = receiver.namespace
|
|
45
50
|
return unless namespace.nil? || namespace.cbase_type?
|
|
@@ -54,13 +59,16 @@ module RuboCop
|
|
|
54
59
|
#: (RuboCop::AST::Node) -> String
|
|
55
60
|
def replacement_for(argument)
|
|
56
61
|
source = argument.source
|
|
62
|
+
|
|
57
63
|
source = "(#{source})" if requires_parentheses?(argument)
|
|
64
|
+
|
|
58
65
|
"#{source}.not_nil!"
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
#: (RuboCop::AST::SendNode, RuboCop::AST::Node, String) -> String
|
|
62
69
|
def correction_for(node, argument, replacement)
|
|
63
|
-
return replacement unless node.multiline?
|
|
70
|
+
return replacement unless node.multiline? && node.parenthesized_call?
|
|
71
|
+
return replacement unless comments_inside_parentheses?(node) || contains_heredoc?(argument)
|
|
64
72
|
|
|
65
73
|
grouped_range = node.source_range.with(begin_pos: node.loc.begin.begin_pos, end_pos: node.loc.end.end_pos)
|
|
66
74
|
grouped_source = grouped_range.source
|
|
@@ -69,11 +77,51 @@ module RuboCop
|
|
|
69
77
|
"#{grouped_source}.not_nil!"
|
|
70
78
|
end
|
|
71
79
|
|
|
80
|
+
#: (RuboCop::AST::Node) -> bool
|
|
81
|
+
def contains_heredoc?(node)
|
|
82
|
+
return true if node.loc.is_a?(Parser::Source::Map::Heredoc)
|
|
83
|
+
|
|
84
|
+
node.each_descendant(:any_str).any? do |descendant|
|
|
85
|
+
descendant.loc.is_a?(Parser::Source::Map::Heredoc)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: (RuboCop::AST::SendNode) -> bool
|
|
90
|
+
def comments_inside_parentheses?(node)
|
|
91
|
+
contents_begin = node.loc.begin.end_pos
|
|
92
|
+
contents_end = node.loc.end.begin_pos
|
|
93
|
+
|
|
94
|
+
processed_source.comments.any? do |comment|
|
|
95
|
+
comment_range = comment.loc.expression
|
|
96
|
+
contents_begin <= comment_range.begin_pos && comment_range.end_pos <= contents_end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
72
100
|
#: (RuboCop::AST::SendNode) -> bool
|
|
73
|
-
def
|
|
74
|
-
node.each_ancestor(:send).any? do |ancestor|
|
|
75
|
-
ancestor.is_a?(RuboCop::AST::SendNode)
|
|
101
|
+
def nested_assertion?(node)
|
|
102
|
+
node.each_ancestor(:send, :block, :numblock, :itblock).any? do |ancestor|
|
|
103
|
+
send_node = ancestor.is_a?(RuboCop::AST::SendNode) ? ancestor : ancestor.send_node
|
|
104
|
+
!send_node.equal?(node) && send_node.is_a?(RuboCop::AST::SendNode) && extract_assertion_argument(send_node)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: (RuboCop::AST::BlockNode, String) -> String
|
|
109
|
+
def commented_correction(block, correction)
|
|
110
|
+
indentation = block.source_range.source_line[/\A\s*/]
|
|
111
|
+
reason_range = block.source_range.with(
|
|
112
|
+
begin_pos: block.loc.begin.end_pos,
|
|
113
|
+
end_pos: block.loc.end.begin_pos,
|
|
114
|
+
)
|
|
115
|
+
reason = reason_range.source
|
|
116
|
+
body = block.body
|
|
117
|
+
if body && (body.str_type? || body.dstr_type?) && ["\"", "'"].include?(body.loc.begin&.source)
|
|
118
|
+
reason.slice!(body.loc.end.begin_pos - reason_range.begin_pos)
|
|
119
|
+
reason.slice!(body.loc.begin.begin_pos - reason_range.begin_pos)
|
|
76
120
|
end
|
|
121
|
+
comments = reason.strip.lines.map { |line| "#{indentation} # #{line.strip}\n" }.join
|
|
122
|
+
return "(\n#{comments}#{correction.delete_prefix("(\n")}" if correction.start_with?("(\n")
|
|
123
|
+
|
|
124
|
+
"(\n#{comments}#{indentation} #{correction}\n#{indentation})"
|
|
77
125
|
end
|
|
78
126
|
|
|
79
127
|
#: (RuboCop::AST::Node) -> bool
|
|
@@ -81,15 +129,21 @@ module RuboCop
|
|
|
81
129
|
return false if argument.begin_type?
|
|
82
130
|
|
|
83
131
|
if argument.is_a?(RuboCop::AST::SendNode)
|
|
132
|
+
return bracket_call_requires_parentheses?(argument) if argument.method?(:[])
|
|
84
133
|
return true if argument.operator_method?
|
|
85
134
|
return true if argument.arguments? && !argument.parenthesized_call?
|
|
86
135
|
end
|
|
87
136
|
return true if argument.range_type? || argument.operator_keyword?
|
|
88
137
|
return true if argument.if_type? || argument.assignment?
|
|
89
|
-
return true if argument.any_block_type?
|
|
90
138
|
|
|
91
139
|
KEYWORD_EXPRESSION_TYPES.include?(argument.type)
|
|
92
140
|
end
|
|
141
|
+
|
|
142
|
+
# `foo[bar]` and `foo.[](bar)` can be chained directly, but command-style `foo.[] bar` cannot.
|
|
143
|
+
#: (RuboCop::AST::SendNode) -> bool
|
|
144
|
+
def bracket_call_requires_parentheses?(argument)
|
|
145
|
+
argument.dot? && !argument.parenthesized_call?
|
|
146
|
+
end
|
|
93
147
|
end
|
|
94
148
|
end
|
|
95
149
|
end
|
data/lib/type_toolkit/dsl.rb
CHANGED
|
@@ -37,7 +37,9 @@ module TypeToolkit
|
|
|
37
37
|
# # is_singleton_method = true, owner is `Foo.singleton_class`
|
|
38
38
|
# abstract def self.foo; end
|
|
39
39
|
# end
|
|
40
|
-
method_owner = is_singleton_method ?
|
|
40
|
+
method_owner = is_singleton_method ? raise(NotImplementedError, <<~MSG) : self
|
|
41
|
+
Abstract singleton methods are not supported yet.
|
|
42
|
+
MSG
|
|
41
43
|
|
|
42
44
|
# Register the fact that this method is meant to be abstract,
|
|
43
45
|
# used by APIs like `abstract_method_declared?` and `Method#abstract?`
|
data/lib/type_toolkit/version.rb
CHANGED
|
@@ -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
|
data/spec/interface_spec.rb
CHANGED
|
@@ -401,5 +401,19 @@ module TypeToolkit
|
|
|
401
401
|
end
|
|
402
402
|
end
|
|
403
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
|
|
404
418
|
end
|
|
405
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
|
|
@@ -12,7 +12,8 @@ module RuboCop
|
|
|
12
12
|
class PreferNotNilSpec < ::Minitest::Spec
|
|
13
13
|
include RuboCop::Minitest::AssertOffense
|
|
14
14
|
|
|
15
|
-
MSG = "TypeToolkit/PreferNotNil:
|
|
15
|
+
MSG = "TypeToolkit/PreferNotNil: #{format(PreferNotNil::MSG, method: :must)}"
|
|
16
|
+
BECAUSE_MSG = "TypeToolkit/PreferNotNil: #{format(PreferNotNil::MSG, method: :must_because)}"
|
|
16
17
|
|
|
17
18
|
before do
|
|
18
19
|
@cop = PreferNotNil.new
|
|
@@ -77,6 +78,8 @@ module RuboCop
|
|
|
77
78
|
^^^^^^^^^^^^^^^^^ #{MSG}
|
|
78
79
|
block_value = T.must(foo { bar })
|
|
79
80
|
^^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
81
|
+
block_with_arguments = T.must(foo(arg) { |value| value })
|
|
82
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
80
83
|
defined_value = T.must(defined?(foo))
|
|
81
84
|
^^^^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
82
85
|
def example
|
|
@@ -98,7 +101,8 @@ module RuboCop
|
|
|
98
101
|
logical = (foo || bar).not_nil!
|
|
99
102
|
grouped = (foo || bar).not_nil!
|
|
100
103
|
assignment = (foo = bar).not_nil!
|
|
101
|
-
block_value =
|
|
104
|
+
block_value = foo { bar }.not_nil!
|
|
105
|
+
block_with_arguments = foo(arg) { |value| value }.not_nil!
|
|
102
106
|
defined_value = (defined?(foo)).not_nil!
|
|
103
107
|
def example
|
|
104
108
|
(yield foo).not_nil!
|
|
@@ -123,6 +127,19 @@ module RuboCop
|
|
|
123
127
|
RUBY
|
|
124
128
|
end
|
|
125
129
|
|
|
130
|
+
it "autocorrects multiline command calls" do
|
|
131
|
+
assert_offense(<<~RUBY)
|
|
132
|
+
T.must foo
|
|
133
|
+
^^^^^^^^^^ #{MSG}
|
|
134
|
+
.bar
|
|
135
|
+
RUBY
|
|
136
|
+
|
|
137
|
+
assert_correction(<<~RUBY)
|
|
138
|
+
foo
|
|
139
|
+
.bar.not_nil!
|
|
140
|
+
RUBY
|
|
141
|
+
end
|
|
142
|
+
|
|
126
143
|
it "does not add unnecessary parentheses to parenthesized calls" do
|
|
127
144
|
assert_offense(<<~RUBY)
|
|
128
145
|
value = T.must(fetch(value))
|
|
@@ -134,6 +151,51 @@ module RuboCop
|
|
|
134
151
|
RUBY
|
|
135
152
|
end
|
|
136
153
|
|
|
154
|
+
it "parenthesizes bracket method calls only when required" do
|
|
155
|
+
assert_offense(<<~RUBY)
|
|
156
|
+
bracketed = T.must(foo[bar]).foo
|
|
157
|
+
^^^^^^^^^^^^^^^^ #{MSG}
|
|
158
|
+
explicit = T.must(foo.[](bar)).foo
|
|
159
|
+
^^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
160
|
+
command = T.must(foo.[] bar).foo
|
|
161
|
+
^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
162
|
+
RUBY
|
|
163
|
+
|
|
164
|
+
assert_correction(<<~RUBY)
|
|
165
|
+
bracketed = foo[bar].not_nil!.foo
|
|
166
|
+
explicit = foo.[](bar).not_nil!.foo
|
|
167
|
+
command = (foo.[] bar).not_nil!.foo
|
|
168
|
+
RUBY
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "does not group an argument that makes the call multiline" do
|
|
172
|
+
assert_offense(<<~RUBY)
|
|
173
|
+
T.must(foo
|
|
174
|
+
^^^^^^^^^^ #{MSG}
|
|
175
|
+
.bar)
|
|
176
|
+
RUBY
|
|
177
|
+
|
|
178
|
+
assert_correction(<<~RUBY)
|
|
179
|
+
foo
|
|
180
|
+
.bar.not_nil!
|
|
181
|
+
RUBY
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "preserves a multiline receiver without parentheses" do
|
|
185
|
+
assert_offense(<<~RUBY)
|
|
186
|
+
T.must(
|
|
187
|
+
^^^^^^^ #{MSG}
|
|
188
|
+
foo.
|
|
189
|
+
bar
|
|
190
|
+
)
|
|
191
|
+
RUBY
|
|
192
|
+
|
|
193
|
+
assert_correction(<<~RUBY)
|
|
194
|
+
foo.
|
|
195
|
+
bar.not_nil!
|
|
196
|
+
RUBY
|
|
197
|
+
end
|
|
198
|
+
|
|
137
199
|
it "preserves comments in multiline calls" do
|
|
138
200
|
assert_offense(<<~RUBY)
|
|
139
201
|
value = T.must(
|
|
@@ -151,7 +213,7 @@ module RuboCop
|
|
|
151
213
|
RUBY
|
|
152
214
|
end
|
|
153
215
|
|
|
154
|
-
it "
|
|
216
|
+
it "drops unnecessary grouping from multiline calls without comments" do
|
|
155
217
|
assert_offense(<<~RUBY)
|
|
156
218
|
first = T .must(
|
|
157
219
|
^^^^^^^^ #{MSG}
|
|
@@ -165,12 +227,21 @@ module RuboCop
|
|
|
165
227
|
RUBY
|
|
166
228
|
|
|
167
229
|
assert_correction(<<~RUBY)
|
|
168
|
-
first =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
230
|
+
first = foo.not_nil!
|
|
231
|
+
second = bar.not_nil!
|
|
232
|
+
RUBY
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "preserves a multiline call chain without parentheses" do
|
|
236
|
+
assert_offense(<<~RUBY)
|
|
237
|
+
variant = T.must(
|
|
238
|
+
^^^^^^^ #{MSG}
|
|
239
|
+
InventoryItemVariant.preload(:variant).where(inventory_item_id: @inventory_item_id).first!.variant
|
|
240
|
+
)
|
|
241
|
+
RUBY
|
|
242
|
+
|
|
243
|
+
assert_correction(<<~RUBY)
|
|
244
|
+
variant = InventoryItemVariant.preload(:variant).where(inventory_item_id: @inventory_item_id).first!.variant.not_nil!
|
|
174
245
|
RUBY
|
|
175
246
|
end
|
|
176
247
|
|
|
@@ -189,6 +260,67 @@ module RuboCop
|
|
|
189
260
|
RUBY
|
|
190
261
|
end
|
|
191
262
|
|
|
263
|
+
it "preserves grouping around heredocs in multiline calls" do
|
|
264
|
+
assert_offense(<<~RUBY)
|
|
265
|
+
value = T.must(
|
|
266
|
+
^^^^^^^ #{MSG}
|
|
267
|
+
<<~TEXT
|
|
268
|
+
hello
|
|
269
|
+
TEXT
|
|
270
|
+
)
|
|
271
|
+
RUBY
|
|
272
|
+
|
|
273
|
+
assert_correction(<<~RUBY)
|
|
274
|
+
value = (
|
|
275
|
+
<<~TEXT
|
|
276
|
+
hello
|
|
277
|
+
TEXT
|
|
278
|
+
).not_nil!
|
|
279
|
+
RUBY
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "preserves grouping around nested heredocs in multiline calls" do
|
|
283
|
+
assert_offense(<<~RUBY)
|
|
284
|
+
value = T.must(
|
|
285
|
+
^^^^^^^ #{MSG}
|
|
286
|
+
foo(<<~TEXT)
|
|
287
|
+
hello
|
|
288
|
+
TEXT
|
|
289
|
+
)
|
|
290
|
+
RUBY
|
|
291
|
+
|
|
292
|
+
assert_correction(<<~RUBY)
|
|
293
|
+
value = (
|
|
294
|
+
foo(<<~TEXT)
|
|
295
|
+
hello
|
|
296
|
+
TEXT
|
|
297
|
+
).not_nil!
|
|
298
|
+
RUBY
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it "preserves grouping around a heredoc T.must nested in another call" do
|
|
302
|
+
assert_offense(<<~RUBY)
|
|
303
|
+
value = foo(
|
|
304
|
+
T.must(
|
|
305
|
+
^^^^^^^ #{MSG}
|
|
306
|
+
<<~TEXT
|
|
307
|
+
hello
|
|
308
|
+
TEXT
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
RUBY
|
|
312
|
+
|
|
313
|
+
assert_correction(<<~RUBY)
|
|
314
|
+
value = foo(
|
|
315
|
+
(
|
|
316
|
+
<<~TEXT
|
|
317
|
+
hello
|
|
318
|
+
TEXT
|
|
319
|
+
).not_nil!
|
|
320
|
+
)
|
|
321
|
+
RUBY
|
|
322
|
+
end
|
|
323
|
+
|
|
192
324
|
it "autocorrects nested T.must calls" do
|
|
193
325
|
assert_offense(<<~RUBY)
|
|
194
326
|
value = T.must(T.must(foo).bar)
|
|
@@ -214,6 +346,104 @@ module RuboCop
|
|
|
214
346
|
RUBY
|
|
215
347
|
end
|
|
216
348
|
|
|
349
|
+
it "reports heredoc reasons without leaving orphaned heredoc bodies" do
|
|
350
|
+
assert_offense(<<~RUBY)
|
|
351
|
+
value = T.must_because(foo) { <<~REASON }
|
|
352
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{BECAUSE_MSG}
|
|
353
|
+
Proven non-nil.
|
|
354
|
+
REASON
|
|
355
|
+
RUBY
|
|
356
|
+
|
|
357
|
+
assert_no_corrections
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it "preserves the reason without attaching the block to the corrected call" do
|
|
361
|
+
assert_offense(<<~RUBY)
|
|
362
|
+
value = T.must_because(foo) { "reason" }.bar
|
|
363
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{BECAUSE_MSG}
|
|
364
|
+
RUBY
|
|
365
|
+
|
|
366
|
+
assert_correction(<<~RUBY)
|
|
367
|
+
value = (
|
|
368
|
+
# reason
|
|
369
|
+
foo.not_nil!
|
|
370
|
+
).bar
|
|
371
|
+
RUBY
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "preserves multiline reasons and argument comments" do
|
|
375
|
+
assert_offense(<<~RUBY)
|
|
376
|
+
value = ::T.must_because(
|
|
377
|
+
^^^^^^^^^^^^^^^^^ #{BECAUSE_MSG}
|
|
378
|
+
# Validated earlier.
|
|
379
|
+
foo,
|
|
380
|
+
) do
|
|
381
|
+
# Explain the invariant.
|
|
382
|
+
"validated for \#{user}"
|
|
383
|
+
end
|
|
384
|
+
RUBY
|
|
385
|
+
|
|
386
|
+
assert_correction(<<~RUBY)
|
|
387
|
+
value = (
|
|
388
|
+
# # Explain the invariant.
|
|
389
|
+
# validated for \#{user}
|
|
390
|
+
# Validated earlier.
|
|
391
|
+
foo
|
|
392
|
+
).not_nil!
|
|
393
|
+
RUBY
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "keeps trailing arguments and string interpolation outside the reason comment" do
|
|
397
|
+
assert_offense(<<~'RUBY', message: BECAUSE_MSG)
|
|
398
|
+
values = [T.must_because(foo) { 'reason' }, bar]
|
|
399
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ %{message}
|
|
400
|
+
text = "#{T.must_because(foo) { "reason" }}"
|
|
401
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ %{message}
|
|
402
|
+
RUBY
|
|
403
|
+
|
|
404
|
+
assert_correction(<<~'RUBY')
|
|
405
|
+
values = [(
|
|
406
|
+
# reason
|
|
407
|
+
foo.not_nil!
|
|
408
|
+
), bar]
|
|
409
|
+
text = "#{(
|
|
410
|
+
# reason
|
|
411
|
+
foo.not_nil!
|
|
412
|
+
)}"
|
|
413
|
+
RUBY
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it "corrects mixed nested assertions without overlapping edits" do
|
|
417
|
+
assert_offense(<<~RUBY)
|
|
418
|
+
value = T.must(T.must_because(T.must(foo)) { "reason" })
|
|
419
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
|
|
420
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{BECAUSE_MSG}
|
|
421
|
+
^^^^^^^^^^^ #{MSG}
|
|
422
|
+
RUBY
|
|
423
|
+
|
|
424
|
+
assert_correction(<<~RUBY)
|
|
425
|
+
value = (
|
|
426
|
+
# reason
|
|
427
|
+
foo.not_nil!.not_nil!
|
|
428
|
+
).not_nil!
|
|
429
|
+
RUBY
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it "comments out assertions in the reason rather than correcting them" do
|
|
433
|
+
assert_offense(<<~RUBY)
|
|
434
|
+
value = T.must_because(foo) { T.must(bar) }
|
|
435
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{BECAUSE_MSG}
|
|
436
|
+
^^^^^^^^^^^ #{MSG}
|
|
437
|
+
RUBY
|
|
438
|
+
|
|
439
|
+
assert_correction(<<~RUBY)
|
|
440
|
+
value = (
|
|
441
|
+
# T.must(bar)
|
|
442
|
+
foo.not_nil!
|
|
443
|
+
)
|
|
444
|
+
RUBY
|
|
445
|
+
end
|
|
446
|
+
|
|
217
447
|
it "ignores other receivers, methods, and argument counts" do
|
|
218
448
|
assert_no_offenses(<<~RUBY)
|
|
219
449
|
Other::T.must(foo)
|
|
@@ -221,6 +451,11 @@ module RuboCop
|
|
|
221
451
|
T.let(foo, String)
|
|
222
452
|
T.must(foo, bar)
|
|
223
453
|
T.must(*values)
|
|
454
|
+
Other::T.must_because(foo) { "reason" }
|
|
455
|
+
object.must_because(foo) { "reason" }
|
|
456
|
+
T.must_because(foo, bar) { "reason" }
|
|
457
|
+
T.must_because(*values) { "reason" }
|
|
458
|
+
T.must_because(foo, &reason)
|
|
224
459
|
RUBY
|
|
225
460
|
end
|
|
226
461
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: type_toolkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Momchilov
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- sorbet/tapioca/require.rb
|
|
133
133
|
- spec/.rubocop.yml
|
|
134
134
|
- spec/interface_spec.rb
|
|
135
|
+
- spec/misc/how_method_added_hooks_work_spec.rb
|
|
135
136
|
- spec/nil_assertions_spec.rb
|
|
136
137
|
- spec/rubocop/cop/type_toolkit/dont_expect_unexpected_nil_spec.rb
|
|
137
138
|
- spec/rubocop/cop/type_toolkit/prefer_not_nil_spec.rb
|