kaizo 0.8.0 → 0.9.2
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/CHANGELOG.md +89 -1
- data/README.md +297 -300
- data/config/default.yml +80 -7
- data/lib/kaizo/version.rb +1 -1
- data/lib/rubocop/cop/kaizo/agent_noun_class_name.rb +23 -0
- data/lib/rubocop/cop/kaizo/argument_counting.rb +17 -7
- data/lib/rubocop/cop/kaizo/explicit_begin.rb +9 -0
- data/lib/rubocop/cop/kaizo/file_utils_inclusion.rb +5 -0
- data/lib/rubocop/cop/kaizo/keyword_arguments.rb +22 -0
- data/lib/rubocop/cop/kaizo/nested_method_calls.rb +12 -0
- data/lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb +11 -0
- data/lib/rubocop/cop/kaizo/plural_collection_name.rb +30 -2
- data/lib/rubocop/cop/kaizo/positional_arguments.rb +16 -0
- data/lib/rubocop/cop/kaizo/prefer_pathname.rb +13 -0
- data/lib/rubocop/cop/kaizo/spec_comment.rb +23 -1
- data/lib/rubocop/cop/kaizo/spec_description_prose.rb +61 -21
- data/lib/rubocop/cop/kaizo/spec_subject.rb +117 -0
- data/lib/rubocop/cop/kaizo/tempfile_create.rb +71 -0
- data/lib/rubocop/cop/kaizo/total_arguments.rb +18 -0
- data/lib/rubocop/cop/kaizo_cops.rb +2 -0
- metadata +4 -2
data/README.md
CHANGED
|
@@ -1,216 +1,229 @@
|
|
|
1
1
|
# Kaizo
|
|
2
2
|
|
|
3
|
+
『スーパーマリオワールド カイゾウ』
|
|
4
|
+
|
|
3
5
|
A strict, punishing [RuboCop](https://rubocop.org) extension aimed at
|
|
4
|
-
AI-agent-authored Ruby — holding generated code to a demanding design bar
|
|
5
|
-
bounding how many arguments a method declares, flagging class names that
|
|
6
|
-
describe an action rather than the concept they model, flagging method calls
|
|
7
|
-
nested too deeply in other calls' arguments, and treating comments and loose
|
|
8
|
-
descriptions in specs as prose that should become structure.
|
|
6
|
+
AI-agent-authored Ruby — holding generated code to a demanding design bar.
|
|
9
7
|
|
|
10
8
|
_Kaizo_ (改造) — "remodeling", "modification": the ruleset keeps applying
|
|
11
|
-
pressure until the code is remade into something better.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
primitives that want to be modeled as an object. By putting a ceiling on the
|
|
15
|
-
number of arguments, these cops apply steady pressure toward naming the
|
|
16
|
-
abstraction — an entity, a value object, a parameter object — instead of
|
|
17
|
-
threading five primitives through a signature.
|
|
9
|
+
pressure until the code is remade into something better. A long argument list
|
|
10
|
+
is the canonical target — loose primitives that want to be modeled as an
|
|
11
|
+
object:
|
|
18
12
|
|
|
19
13
|
```ruby
|
|
20
|
-
# bad
|
|
14
|
+
# bad — four loose primitives thread through the signature
|
|
21
15
|
def calculate_volume(width, length, height, shape_type)
|
|
22
16
|
end
|
|
23
17
|
|
|
24
|
-
# good
|
|
18
|
+
# good — the abstraction has a name
|
|
25
19
|
def calculate_volume(shape)
|
|
26
20
|
end
|
|
27
21
|
```
|
|
28
22
|
|
|
23
|
+
```console
|
|
24
|
+
$ rubocop --only Kaizo volume.rb
|
|
25
|
+
volume.rb:1:5: C: Kaizo/PositionalArguments: Method has too many positional arguments. [4/1]
|
|
26
|
+
def calculate_volume(width, length, height, shape_type)
|
|
27
|
+
^^^^^^^^^^^^^^^^
|
|
28
|
+
volume.rb:1:5: C: Kaizo/TotalArguments: Method has too many arguments. [4/2]
|
|
29
|
+
def calculate_volume(width, length, height, shape_type)
|
|
30
|
+
^^^^^^^^^^^^^^^^
|
|
31
|
+
|
|
32
|
+
1 file inspected, 2 offenses detected
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every example in this README is executed against the shipped configuration by
|
|
36
|
+
the test suite: `# bad` code really is flagged by the named cop, `# good` code
|
|
37
|
+
really passes every cop, every YAML snippet is valid config, and the terminal
|
|
38
|
+
output above is re-derived from the cops themselves. Where a section covers
|
|
39
|
+
several cops, each bad example names the one that fires.
|
|
40
|
+
|
|
29
41
|
## Cops
|
|
30
42
|
|
|
31
|
-
| Cop |
|
|
32
|
-
|
|
33
|
-
| `Kaizo/PositionalArguments` |
|
|
34
|
-
| `Kaizo/KeywordArguments` |
|
|
35
|
-
| `Kaizo/TotalArguments` |
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
prefers `Pathname` over `File` for the operations `Pathname` provides (see
|
|
61
|
-
[Prefer Pathname](#prefer-pathname)), **`Kaizo/ExplicitBegin`**, which requires
|
|
62
|
-
an explicit `begin` for method bodies that `rescue` or `ensure` (see
|
|
63
|
-
[Explicit begin](#explicit-begin)), and **`Kaizo/NextInNonVoidEnumerable`**,
|
|
64
|
-
which flags `next` used as control-flow-as-value inside `map`/`select`/`reduce`
|
|
65
|
-
blocks (see [Next in value-returning blocks](#next-in-value-returning-blocks)).
|
|
43
|
+
| Cop | Flags |
|
|
44
|
+
|-----|-------|
|
|
45
|
+
| [`Kaizo/PositionalArguments`](#argument-counts) | more than `Max` positional parameters |
|
|
46
|
+
| [`Kaizo/KeywordArguments`](#argument-counts) | more than `Max` keyword parameters |
|
|
47
|
+
| [`Kaizo/TotalArguments`](#argument-counts) | more than `Max` parameters in total |
|
|
48
|
+
| [`Kaizo/AgentNounClassName`](#class-naming) | classes named for what they do, not what they model |
|
|
49
|
+
| [`Kaizo/NestedMethodCalls`](#nested-method-calls) | calls buried in other calls' arguments |
|
|
50
|
+
| [`Kaizo/SpecComment`](#comments-in-specs) | comments in spec files |
|
|
51
|
+
| [`Kaizo/SpecDescriptionProse`](#spec-description-prose) | descriptions that are lists, conditions, or code |
|
|
52
|
+
| [`Kaizo/SpecSubject`](#spec-subject) | the unit under test hidden in a `let` |
|
|
53
|
+
| [`Kaizo/FileUtilsInclusion`](#including-fileutils) | repeated `FileUtils.` qualification |
|
|
54
|
+
| [`Kaizo/PreferPathname`](#prefer-pathname) | `File` class methods `Pathname` already provides |
|
|
55
|
+
| [`Kaizo/TempfileCreate`](#temp-files) | temp-file APIs with nondeterministic cleanup |
|
|
56
|
+
| [`Kaizo/ExplicitBegin`](#explicit-begin) | `rescue`/`ensure` attached straight to `def` |
|
|
57
|
+
| [`Kaizo/NextInNonVoidEnumerable`](#next-in-value-returning-blocks) | `next` as a value in `map`/`select`/`reduce` |
|
|
58
|
+
| [`Kaizo/PluralCollectionName`](#plural-names-for-collections) | arrays returned under singular names |
|
|
59
|
+
|
|
60
|
+
**Every cop ships enabled.** `plugins: [kaizo]` turns all fourteen on at
|
|
61
|
+
their strict defaults — there is nothing to opt into and no pending status.
|
|
62
|
+
Outside its own department kaizo touches two core cops: it disables
|
|
63
|
+
`Style/RedundantBegin` ([Explicit begin](#explicit-begin)) and sets
|
|
64
|
+
`Style/HashSyntax` to enforce Ruby 3.1's hash-value shorthand —
|
|
65
|
+
`Session.new(table:)` over `Session.new(table: table)`.
|
|
66
|
+
|
|
67
|
+
The three argument cops are independent dimensions — enable the smallest set
|
|
68
|
+
that expresses your rule; a method breaking several bounds is reported once
|
|
69
|
+
per cop. None of the cops autocorrect — every fix is a design decision — with
|
|
70
|
+
one exception: `Kaizo/ExplicitBegin`, whose `begin`/`end` wrap is mechanical
|
|
71
|
+
(`rubocop -a`).
|
|
66
72
|
|
|
67
73
|
## Installation
|
|
68
74
|
|
|
69
|
-
Add to your `Gemfile`:
|
|
70
|
-
|
|
71
75
|
```ruby
|
|
72
76
|
gem 'kaizo', require: false
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
Enable the plugin in `.rubocop.yml`:
|
|
76
|
-
|
|
77
79
|
```yaml
|
|
78
80
|
plugins:
|
|
79
81
|
- kaizo
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
Requires RuboCop 1.72.2+ for the `lint_roller` plugin API.
|
|
83
85
|
|
|
84
|
-
##
|
|
86
|
+
## Argument counts
|
|
85
87
|
|
|
86
|
-
The defaults are deliberately strict —
|
|
87
|
-
argument
|
|
88
|
-
too aggressive for your codebase:
|
|
88
|
+
The defaults are deliberately strict — at most one positional and one keyword
|
|
89
|
+
argument. Loosen them, or set a `Max` to `0` to forbid that kind entirely:
|
|
89
90
|
|
|
90
91
|
```yaml
|
|
91
92
|
Kaizo/PositionalArguments:
|
|
92
|
-
Max: 1 # default
|
|
93
|
+
Max: 1 # default; 0 forces every argument to be a keyword
|
|
93
94
|
Kaizo/KeywordArguments:
|
|
94
95
|
Max: 1 # default
|
|
95
96
|
Kaizo/TotalArguments:
|
|
96
97
|
Max: 2 # default (one positional + one keyword)
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
All three check `def`, `def self.`, `define_method`, and
|
|
101
|
+
`define_singleton_method`. Required and optional parameters count alike:
|
|
101
102
|
|
|
102
|
-
```
|
|
103
|
-
Kaizo/
|
|
104
|
-
|
|
103
|
+
```ruby
|
|
104
|
+
# bad — Kaizo/TotalArguments: two positional plus two keyword exceed Max 2
|
|
105
|
+
def route(verb, path, to:, name: nil)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# bad — Kaizo/KeywordArguments: three keyword parameters exceed Max 1
|
|
109
|
+
def connect(host:, port:, scheme:)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# good — Kaizo/KeywordArguments counts none of these: collectors are single
|
|
113
|
+
# tokens, not lists of primitives
|
|
114
|
+
def log(*messages, **context, &formatter)
|
|
115
|
+
end
|
|
105
116
|
```
|
|
106
117
|
|
|
107
|
-
|
|
118
|
+
The keyword-counting cops skip `spec/` and `test/` trees entirely: wide
|
|
119
|
+
keyword interfaces are the testing idiom — FactoryBot's `create`/`build`,
|
|
120
|
+
custom builder helpers — and keywords communicate fine at any width there.
|
|
121
|
+
Positional pressure is universal, because positional arguments communicate
|
|
122
|
+
nothing unless they are solo:
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
```ruby
|
|
125
|
+
# good — spec/support/builders.rb: a wide keyword builder is normal test
|
|
126
|
+
# infrastructure, so Kaizo/KeywordArguments and Kaizo/TotalArguments skip it
|
|
127
|
+
def create_order(customer:, items:, coupon: nil, shipping: :standard)
|
|
128
|
+
Order.create(customer:, items:, coupon:, shipping:)
|
|
129
|
+
end
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
# bad — Kaizo/PositionalArguments: spec/support/builders.rb is not exempt
|
|
132
|
+
# from positional pressure; three anonymous values say nothing
|
|
133
|
+
def build_order(customer, items, coupon)
|
|
134
|
+
Order.create(customer:, items:, coupon:)
|
|
135
|
+
end
|
|
136
|
+
```
|
|
113
137
|
|
|
114
|
-
|
|
115
|
-
counted — they are single tokens, not a list of primitives.
|
|
138
|
+
Police tests like everything else by clearing the exclusion:
|
|
116
139
|
|
|
117
|
-
|
|
140
|
+
```yaml
|
|
141
|
+
Kaizo/KeywordArguments:
|
|
142
|
+
Exclude: [] # count keywords in tests too
|
|
143
|
+
Kaizo/TotalArguments:
|
|
144
|
+
Exclude: []
|
|
145
|
+
```
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
parameters mirror the value object's attributes — which is exactly the modeling
|
|
121
|
-
these cops are meant to encourage:
|
|
147
|
+
Two shapes are structurally exempt:
|
|
122
148
|
|
|
123
149
|
```ruby
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
# good — a Struct/Data initialize mirrors the value object's attributes,
|
|
151
|
+
# which is exactly the modeling these cops push toward
|
|
152
|
+
Data.define(:width, :height, :depth) do
|
|
153
|
+
def initialize(width:, height:, depth:)
|
|
127
154
|
super
|
|
128
155
|
end
|
|
129
156
|
end
|
|
130
|
-
```
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
extract and no primitive obsession to correct. The same holds for `[]`, `<=>`,
|
|
135
|
-
`+`, `<<`, `==`, and the rest of the operator family:
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
# not flagged
|
|
158
|
+
# good — operator arity is fixed by Ruby's syntax; there is no object to
|
|
159
|
+
# extract (`[]`, `<=>`, `+`, `<<`, and the rest of the family likewise)
|
|
139
160
|
def []=(row, column, value)
|
|
140
161
|
@cells[row][column] = value
|
|
141
162
|
end
|
|
142
163
|
```
|
|
143
164
|
|
|
144
|
-
|
|
145
|
-
name is computed at runtime is still checked — the cop cannot know what the name
|
|
146
|
-
resolves to. Note the exemption is for *operator* methods, not ordinary writers:
|
|
147
|
-
`def name=(value)` takes a single argument and was never in danger of tripping the
|
|
148
|
-
limits anyway.
|
|
165
|
+
Beyond the structural exemptions, exempt methods by name or pattern:
|
|
149
166
|
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
```yaml
|
|
168
|
+
Kaizo/KeywordArguments:
|
|
169
|
+
Max: 1
|
|
170
|
+
AllowedMethods:
|
|
171
|
+
- initialize # constructors may gather collaborators
|
|
172
|
+
Kaizo/PositionalArguments:
|
|
173
|
+
AllowedPatterns:
|
|
174
|
+
- '\Abuild_' # or exempt a whole naming family
|
|
175
|
+
```
|
|
152
176
|
|
|
153
|
-
|
|
177
|
+
### `define_method` edge cases
|
|
154
178
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
```ruby
|
|
180
|
+
# good — only the block form of define_method is inspected; a callable body
|
|
181
|
+
# may be any object that responds to call, and is not statically countable
|
|
182
|
+
define_method(:resize, ->(width, height) { @size = [width, height] })
|
|
159
183
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
184
|
+
# good — numbered and `it` parameters are not a declared signature, so they
|
|
185
|
+
# count as zero; spell parameters out if you want them counted
|
|
186
|
+
define_method(:squared) { _1 * _1 }
|
|
163
187
|
|
|
164
|
-
|
|
188
|
+
# bad — Kaizo/TotalArguments: a name computed at runtime is still checked;
|
|
189
|
+
# the declared parameters matter, not how the name is spelled
|
|
190
|
+
define_method(:"handle_#{event}") { |source, payload, context| dispatch(source) }
|
|
191
|
+
```
|
|
165
192
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
- **Numbered and `it` block parameters count as zero.**
|
|
173
|
-
`define_method(:squared) { _1 * _1 }` is treated as taking no arguments:
|
|
174
|
-
implicit block parameters are not part of a declared signature, which is what
|
|
175
|
-
these cops measure. Spell the parameters out if you want them counted.
|
|
193
|
+
## Relationship to `Metrics/ParameterLists`
|
|
194
|
+
|
|
195
|
+
Core's `Metrics/ParameterLists` caps the whole parameter list; kaizo bounds
|
|
196
|
+
positional and keyword arguments separately and is framed around domain
|
|
197
|
+
modeling. They can coexist. Full rationale with reproducible evidence:
|
|
198
|
+
[docs/why-not-metrics-parameterlists.md](docs/why-not-metrics-parameterlists.md).
|
|
176
199
|
|
|
177
200
|
## Class naming
|
|
178
201
|
|
|
179
202
|
`Kaizo/AgentNounClassName` flags classes named as agent nouns — "doers" —
|
|
180
|
-
rather than the domain concepts they model
|
|
181
|
-
|
|
182
|
-
forbidden suffix like `Service`, usually means procedural behavior that wants a
|
|
183
|
-
clearer name or a different home.
|
|
203
|
+
rather than the domain concepts they model: names ending in `er`/`or`, or in
|
|
204
|
+
a configured forbidden suffix like `Service`.
|
|
184
205
|
|
|
185
206
|
```ruby
|
|
186
|
-
# bad
|
|
207
|
+
# bad — an -er name describes behavior, not a concept
|
|
187
208
|
class PaymentProcessor
|
|
188
209
|
end
|
|
189
210
|
|
|
211
|
+
# bad — Struct/Data/Class constant assignments are checked too
|
|
212
|
+
RequestHandler = Data.define(:request)
|
|
213
|
+
|
|
190
214
|
# good
|
|
191
215
|
class Payment
|
|
192
216
|
end
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
It checks `class` definitions and `Struct.new` / `Data.define` / `Class.new`
|
|
196
|
-
constant assignments. Like the argument-count cops, there is **no autocorrection** — a
|
|
197
|
-
rename is a design decision.
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`InvoiceBuilder`):
|
|
204
|
-
|
|
205
|
-
- **`AllowedSuffixes`** — exempt these. Matched as a suffix, so `Controller`
|
|
206
|
-
clears `Controller` and `UsersController` alike. Ships with a broad default of
|
|
207
|
-
legitimate `-er`/`-or` words — domain nouns (`Order`, `User`, `Number`,
|
|
208
|
-
`Error`) and framework terms (`Controller`, `Serializer`, `Adapter`).
|
|
209
|
-
- **`ForbiddenSuffixes`** — always flag these, even when they don't end in
|
|
210
|
-
`-er`/`-or` (default: `Service`, `Util`, `Utils`). This list **wins** over
|
|
211
|
-
`AllowedSuffixes`, so it doubles as the way to drop a default exemption.
|
|
218
|
+
# good — ends in an allowed suffix
|
|
219
|
+
class UsersController
|
|
220
|
+
end
|
|
221
|
+
```
|
|
212
222
|
|
|
213
|
-
|
|
223
|
+
Two configurable suffix lists drive it, matched against the last segment of a
|
|
224
|
+
namespaced name. `AllowedSuffixes` exempts legitimate `-er`/`-or` words and
|
|
225
|
+
ships with a broad default (`Adapter`, `Controller`, `Error`, `User`, ...);
|
|
226
|
+
`ForbiddenSuffixes` always wins, which is how a default exemption is dropped:
|
|
214
227
|
|
|
215
228
|
```yaml
|
|
216
229
|
Kaizo/AgentNounClassName:
|
|
@@ -219,44 +232,32 @@ Kaizo/AgentNounClassName:
|
|
|
219
232
|
- AllowedSuffixes
|
|
220
233
|
- ForbiddenSuffixes
|
|
221
234
|
AllowedSuffixes:
|
|
222
|
-
-
|
|
235
|
+
- Voucher # PaymentVoucher now passes
|
|
223
236
|
ForbiddenSuffixes:
|
|
224
237
|
- Server # ApiServer now flagged, despite the default allowance
|
|
225
238
|
```
|
|
226
239
|
|
|
227
240
|
## Nested method calls
|
|
228
241
|
|
|
229
|
-
`Kaizo/NestedMethodCalls` flags
|
|
230
|
-
positions —
|
|
231
|
-
intermediate results want names. Reaching for the right name (or extracting a
|
|
232
|
-
method) almost always reads better, and is easier to debug, than peeling
|
|
233
|
-
parentheses apart.
|
|
234
|
-
|
|
235
|
-
The point is not the assignment, it is the **name**. A local called `result` or
|
|
236
|
-
`tmp` buys nothing; a name that says what the value *is* turns the step into its
|
|
237
|
-
own documentation.
|
|
242
|
+
`Kaizo/NestedMethodCalls` flags calls nested too deeply in **argument**
|
|
243
|
+
positions — intermediate results want names.
|
|
238
244
|
|
|
239
245
|
```ruby
|
|
240
246
|
# bad
|
|
241
247
|
wrap(parse(read(io)))
|
|
242
248
|
|
|
243
|
-
#
|
|
244
|
-
result = parse(read(io))
|
|
245
|
-
wrap(result)
|
|
246
|
-
|
|
247
|
-
# good - the name documents what the value is
|
|
249
|
+
# good — the name turns the step into its own documentation
|
|
248
250
|
parsed_config = parse(read(io))
|
|
249
251
|
wrap(parsed_config)
|
|
250
252
|
|
|
251
|
-
# good
|
|
253
|
+
# good — a single nested call is fine at the default Max
|
|
252
254
|
puts compute(value)
|
|
253
255
|
```
|
|
254
256
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`user.account.owner
|
|
258
|
-
|
|
259
|
-
traversed, and `AllowedMethods` exempts calls to named methods:
|
|
257
|
+
A local called `result` or `tmp` satisfies the cop but not the reader — the
|
|
258
|
+
point is the name. Only argument nesting counts: receiver chains
|
|
259
|
+
(`user.account.owner`) are a separate concern, operator methods never count,
|
|
260
|
+
and block bodies are not traversed.
|
|
260
261
|
|
|
261
262
|
```yaml
|
|
262
263
|
Kaizo/NestedMethodCalls:
|
|
@@ -265,16 +266,11 @@ Kaizo/NestedMethodCalls:
|
|
|
265
266
|
- expect # e.g. don't count RSpec's expect(...) wrapper
|
|
266
267
|
```
|
|
267
268
|
|
|
268
|
-
Like the other cops, there is **no autocorrection** — choosing the intermediate
|
|
269
|
-
name is a design decision.
|
|
270
|
-
|
|
271
269
|
## Comments in specs
|
|
272
270
|
|
|
273
|
-
`Kaizo/SpecComment` flags comments in spec files
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
wants to be a `context`/`it` description, a clearer example name, or another
|
|
277
|
-
example — not prose riding alongside the code.
|
|
271
|
+
`Kaizo/SpecComment` flags comments in spec files: a sentence explaining an
|
|
272
|
+
example usually wants to be a `context`/`it` description, a clearer example
|
|
273
|
+
name, or another example.
|
|
278
274
|
|
|
279
275
|
```ruby
|
|
280
276
|
# bad
|
|
@@ -291,17 +287,9 @@ it 'permits an admin to see everything' do
|
|
|
291
287
|
end
|
|
292
288
|
```
|
|
293
289
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
the other cops, there is **no autocorrection** — turning an explanation into a
|
|
298
|
-
spec is a design decision.
|
|
299
|
-
|
|
300
|
-
### Scope and escape hatches
|
|
301
|
-
|
|
302
|
-
The cop is scoped through its `Include`, so broaden it to cover support files or a
|
|
303
|
-
Minitest suite (using `inherit_mode: merge` to add to the default rather than
|
|
304
|
-
replace it):
|
|
290
|
+
Only `*_spec.rb` files are inspected; `spec/helpers/` and `spec/support/`
|
|
291
|
+
hold infrastructure, not specs, and are excluded by default. Magic comments,
|
|
292
|
+
`# rubocop:` directives, and shebangs are never flagged.
|
|
305
293
|
|
|
306
294
|
```yaml
|
|
307
295
|
Kaizo/SpecComment:
|
|
@@ -311,69 +299,94 @@ Kaizo/SpecComment:
|
|
|
311
299
|
Include:
|
|
312
300
|
- '**/spec/**/*' # spec_helper, support/, factories
|
|
313
301
|
- '**/*_test.rb' # Minitest / Test::Unit
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
Permit specific comments with `AllowedPatterns` — regexps matched against the
|
|
317
|
-
full comment text, leading `#` included:
|
|
318
|
-
|
|
319
|
-
```yaml
|
|
320
|
-
Kaizo/SpecComment:
|
|
302
|
+
Exclude: [] # police spec/helpers and spec/support too
|
|
321
303
|
AllowedPatterns:
|
|
322
|
-
- '\A#\s*@rbs' # rbs-inline type annotations
|
|
323
|
-
- 'noqa'
|
|
304
|
+
- '\A#\s*@rbs' # permit rbs-inline type annotations
|
|
324
305
|
```
|
|
325
306
|
|
|
326
307
|
## Spec description prose
|
|
327
308
|
|
|
328
|
-
`Kaizo/SpecDescriptionProse` requires
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
An `it`/`specify`/`example` description must not contain:
|
|
334
|
-
|
|
335
|
-
- a **comma** — a list is several behaviors;
|
|
336
|
-
- a **conjunction** (`and`, `or`, `so`, `when`, `if`, `unless`, … — the
|
|
337
|
-
`Conjunctions` list) — joined clauses are separate examples, and a condition
|
|
338
|
-
belongs in a `context`;
|
|
339
|
-
- **code** — `_ : # = { } ! [ ]`, a backtick, or a nested quoted literal;
|
|
340
|
-
a description is prose, not identifiers or wire values.
|
|
341
|
-
|
|
342
|
-
A `context` description must not contain code, and must open with a word from
|
|
343
|
-
`ContextPrefixes` (`when`/`with`/`without`/`after`). `describe` strings name the
|
|
344
|
-
unit under test and are exempt.
|
|
309
|
+
`Kaizo/SpecDescriptionProse` requires `it`/`context` descriptions to read as
|
|
310
|
+
one-behavior prose. Every rule is structural — it fires only when the wording
|
|
311
|
+
signals that one example is really several, or that the assertion is leaking
|
|
312
|
+
into the name.
|
|
345
313
|
|
|
346
314
|
```ruby
|
|
347
315
|
# bad
|
|
348
|
-
it "renders the name, image, and flag"
|
|
349
|
-
it "omits the key when the role is unset"
|
|
350
|
-
it "renders the :cpu member"
|
|
351
|
-
context "the role is unset" do
|
|
316
|
+
it "renders the name, image, and flag" # a comma joins several behaviors
|
|
317
|
+
it "omits the key when the role is unset" # a condition belongs in a context
|
|
318
|
+
it "renders the :cpu member" # code is not prose
|
|
319
|
+
context "the role is unset" do # contexts open with when/with/without/after
|
|
352
320
|
end
|
|
353
321
|
|
|
354
322
|
# good
|
|
355
323
|
it "renders the name"
|
|
356
324
|
it "renders the cpu member"
|
|
325
|
+
it "raises Timeout::Error"
|
|
357
326
|
context "when the role is unset" do
|
|
358
327
|
it "omits the key"
|
|
359
328
|
end
|
|
360
329
|
```
|
|
361
330
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
331
|
+
`describe` strings name the unit under test and are exempt. So is an error
|
|
332
|
+
class name (`raises Timeout::Error` above) — the error is what the user
|
|
333
|
+
ultimately sees, so it *is* the specified behavior. The `ForbiddenWords`
|
|
334
|
+
defaults are curated, not exhaustive: `for` is a preposition in most
|
|
335
|
+
descriptions, and homographs like `given` collide with prose, so they are
|
|
336
|
+
left out — add them back if you want them:
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
Kaizo/SpecDescriptionProse:
|
|
340
|
+
inherit_mode:
|
|
341
|
+
merge:
|
|
342
|
+
- ForbiddenWords
|
|
343
|
+
- AllowedPatterns
|
|
344
|
+
ForbiddenWords:
|
|
345
|
+
- given # flag `given ...` descriptions too
|
|
346
|
+
AllowedPatterns:
|
|
347
|
+
- 'Foo::Widget' # this one identifier is allowed anywhere
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Before 0.9 these lists were named `Conjunctions` and `ContextPrefixes`; the
|
|
351
|
+
old keys are no longer read.
|
|
352
|
+
|
|
353
|
+
## Spec subject
|
|
354
|
+
|
|
355
|
+
`Kaizo/SpecSubject` requires the unit under test to be declared with
|
|
356
|
+
`subject`, not hidden in a `let` — `subject` is RSpec's name for the object
|
|
357
|
+
being specified, and declaring it unlocks `is_expected` one-liners.
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# bad
|
|
361
|
+
RSpec.describe Session::Pool do
|
|
362
|
+
let(:pool) { described_class.new }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# good
|
|
366
|
+
RSpec.describe Session::Pool do
|
|
367
|
+
subject(:pool) { described_class.new }
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
A `let` is flagged only when its block confidently builds the class under
|
|
372
|
+
test: a `.new` of `described_class`, of the constant an enclosing
|
|
373
|
+
`describe`/`context` names (full or short name), or of a constant matching
|
|
374
|
+
the spec's filename (`pool_spec.rb` names `Pool`). Deliberate second
|
|
375
|
+
instances are the escape hatch's job:
|
|
376
|
+
|
|
377
|
+
```yaml
|
|
378
|
+
Kaizo/SpecSubject:
|
|
379
|
+
AllowedMethods:
|
|
380
|
+
- other # subject == other comparisons
|
|
381
|
+
AllowedPatterns:
|
|
382
|
+
- '\Aother_'
|
|
383
|
+
```
|
|
370
384
|
|
|
371
385
|
## Including FileUtils
|
|
372
386
|
|
|
373
|
-
`Kaizo/FileUtilsInclusion` flags
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
methods unqualified.
|
|
387
|
+
`Kaizo/FileUtilsInclusion` flags a class or module (reported once) that
|
|
388
|
+
qualifies `FileUtils.` more than once: `include` it for instance-level use,
|
|
389
|
+
`extend` it for class-level use, and call the methods unqualified.
|
|
377
390
|
|
|
378
391
|
```ruby
|
|
379
392
|
# bad
|
|
@@ -395,17 +408,13 @@ class Backup
|
|
|
395
408
|
end
|
|
396
409
|
```
|
|
397
410
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
nested class do not add up). As with most of the cops here, there is **no
|
|
402
|
-
autocorrection** — whether to `include` or `extend`, and where the mixin belongs,
|
|
403
|
-
is a design decision.
|
|
411
|
+
A single qualified call is left alone, a namespace already mixing in
|
|
412
|
+
`FileUtils` is not flagged, and nested classes are counted on their own.
|
|
413
|
+
|
|
404
414
|
## Prefer Pathname
|
|
405
415
|
|
|
406
|
-
`Kaizo/PreferPathname` flags
|
|
407
|
-
instance-method equivalent — `
|
|
408
|
-
`File.expand_path`, and the like. Once a path is a `Pathname`, calling the method
|
|
416
|
+
`Kaizo/PreferPathname` flags `File` class methods with a `Pathname`
|
|
417
|
+
instance-method equivalent — once a path is a `Pathname`, calling the method
|
|
409
418
|
on it reads better than threading a string through `File`.
|
|
410
419
|
|
|
411
420
|
```ruby
|
|
@@ -420,12 +429,12 @@ path.exist?
|
|
|
420
429
|
dir.join(name)
|
|
421
430
|
```
|
|
422
431
|
|
|
423
|
-
The banned set is the intersection of `File`'s class methods and `Pathname`'s
|
|
424
|
-
public instance methods
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
432
|
+
The banned set is the intersection of `File`'s class methods and `Pathname`'s
|
|
433
|
+
public instance methods, so `File.new` is left alone. A few equivalents are
|
|
434
|
+
not drop-in — `Pathname#join` treats an absolute segment as a reset,
|
|
435
|
+
`Pathname#chmod` acts on one receiver where `File.chmod` is variadic — which
|
|
436
|
+
is part of why there is no autocorrection. Executables often work with raw
|
|
437
|
+
path strings, so `exe/**/*` and `bin/**/*` are skipped by default:
|
|
429
438
|
|
|
430
439
|
```yaml
|
|
431
440
|
Kaizo/PreferPathname:
|
|
@@ -435,25 +444,29 @@ Kaizo/PreferPathname:
|
|
|
435
444
|
- 'db/**/*' # add your own
|
|
436
445
|
```
|
|
437
446
|
|
|
438
|
-
|
|
439
|
-
`Pathname#join` treats an absolute segment as a reset (`Pathname("a").join("/b")`
|
|
440
|
-
is `/b`, where `File.join("a", "/b")` is `a/b`), `Pathname#chmod`/`chown`/`utime`
|
|
441
|
-
act on the single receiver (where `File.chmod` is variadic over many paths), and
|
|
442
|
-
`Pathname#split`/`rename` differ in return type and arity. The cop only points;
|
|
443
|
-
mind those differences when you rewrite — part of why it does not autocorrect.
|
|
447
|
+
## Temp files
|
|
444
448
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
449
|
+
`Kaizo/TempfileCreate` requires block-form `Tempfile.create` — the only
|
|
450
|
+
temp-file API whose cleanup is deterministic.
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
# bad
|
|
454
|
+
file = Tempfile.new("report") # removed in a GC finalizer, or never
|
|
455
|
+
file = Tempfile.open("report") # the same finalizer gamble
|
|
456
|
+
file = Tempfile.create("report") # a bare File that is never auto-removed
|
|
457
|
+
|
|
458
|
+
# good — closed and removed when the block returns, however it returns
|
|
459
|
+
Tempfile.create("report") do |file|
|
|
460
|
+
file.write(data)
|
|
461
|
+
end
|
|
462
|
+
```
|
|
448
463
|
|
|
449
464
|
## Explicit begin
|
|
450
465
|
|
|
451
466
|
`Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
|
|
452
|
-
body attaches
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
`rescue`/`ensure` covers, so the method can grow other statements without
|
|
456
|
-
silently widening what is rescued.
|
|
467
|
+
body attaches `rescue`/`ensure` directly to the `def`: the `begin` marks
|
|
468
|
+
exactly what is guarded, so the method can grow without silently widening
|
|
469
|
+
what the `rescue` covers.
|
|
457
470
|
|
|
458
471
|
```ruby
|
|
459
472
|
# bad
|
|
@@ -473,30 +486,30 @@ def foo
|
|
|
473
486
|
end
|
|
474
487
|
```
|
|
475
488
|
|
|
476
|
-
Modifier rescues (`foo rescue nil`) and endless
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
and `end` (a single-line definition, say), or contains a heredoc or other
|
|
481
|
-
multiline string, symbol, or regexp literal, where re-indenting could change
|
|
482
|
-
their contents.
|
|
489
|
+
Modifier rescues (`foo rescue nil`) and endless definitions are not flagged.
|
|
490
|
+
This is the one cop that autocorrects (`rubocop -a`); the correction skips
|
|
491
|
+
single-line definitions and bodies holding heredocs or other multiline
|
|
492
|
+
literals, where re-indenting could change their contents.
|
|
483
493
|
|
|
484
|
-
Because `Style/RedundantBegin` enforces the exact opposite style,
|
|
485
|
-
plugin
|
|
486
|
-
forever
|
|
487
|
-
|
|
494
|
+
Because core's `Style/RedundantBegin` enforces the exact opposite style,
|
|
495
|
+
loading this plugin disables it — otherwise the two autocorrections would
|
|
496
|
+
loop forever. To opt out of explicit begins, disable this cop — re-enabling
|
|
497
|
+
`Style/RedundantBegin` alone would leave both cops on, each flagging the form
|
|
498
|
+
the other mandates:
|
|
488
499
|
|
|
489
500
|
```yaml
|
|
501
|
+
Kaizo/ExplicitBegin:
|
|
502
|
+
Enabled: false # opt out of explicit begins
|
|
490
503
|
Style/RedundantBegin:
|
|
491
|
-
Enabled: true #
|
|
504
|
+
Enabled: true # optional: enforce the inverse style instead
|
|
492
505
|
```
|
|
493
506
|
|
|
494
507
|
## Next in value-returning blocks
|
|
495
508
|
|
|
496
509
|
`Kaizo/NextInNonVoidEnumerable` flags `next` inside the block of a
|
|
497
|
-
value-returning `Enumerable` method — `map`, `select`, `filter_map`,
|
|
498
|
-
`
|
|
499
|
-
|
|
510
|
+
value-returning `Enumerable` method — `map`, `select`, `filter_map`,
|
|
511
|
+
`reduce`, `sum`, the `*_by` methods, the predicates — where `next` is
|
|
512
|
+
control flow being used as a value.
|
|
500
513
|
|
|
501
514
|
```ruby
|
|
502
515
|
# bad
|
|
@@ -505,47 +518,37 @@ array.map do |item|
|
|
|
505
518
|
transform(item)
|
|
506
519
|
end
|
|
507
520
|
|
|
508
|
-
# bad
|
|
521
|
+
# bad — `next <value>` counts too
|
|
509
522
|
array.reduce(0) do |sum, item|
|
|
510
523
|
next sum if skip?(item)
|
|
511
524
|
sum + item
|
|
512
525
|
end
|
|
513
526
|
|
|
514
|
-
# good
|
|
527
|
+
# good — a void iteration method; `next` just skips the iteration
|
|
515
528
|
array.each do |item|
|
|
516
529
|
next if skip?(item)
|
|
517
530
|
process(item)
|
|
518
531
|
end
|
|
519
532
|
|
|
520
|
-
# good
|
|
533
|
+
# good — say what you mean
|
|
521
534
|
array.filter_map { |item| transform(item) unless skip?(item) }
|
|
522
535
|
```
|
|
523
536
|
|
|
524
|
-
|
|
525
|
-
(`
|
|
526
|
-
|
|
527
|
-
flagged set. Non-`Enumerable` looping constructs (`loop`, `Integer#times`,
|
|
528
|
-
`while`) are likewise never flagged. A `next` that binds to a nested block or
|
|
529
|
-
loop is attributed to that inner scope, so an inner `each { next }` or
|
|
530
|
-
`while … next … end` does not flag an outer `map`.
|
|
531
|
-
|
|
532
|
-
As with most of the cops here, there is **no autocorrection** — the right fix
|
|
533
|
-
depends on intent (a guard clause might become a ternary, a `select`/`reject`, a
|
|
534
|
-
`filter_map`, or a restructured block). Exempt specific methods with
|
|
535
|
-
`AllowedMethods` / `AllowedPatterns`:
|
|
537
|
+
Void iteration methods (`each`, `each_with_object`, ...) and non-`Enumerable`
|
|
538
|
+
loops (`loop`, `while`, `Integer#times`) are never flagged, and a `next`
|
|
539
|
+
bound to a nested block or loop is attributed to that inner scope.
|
|
536
540
|
|
|
537
541
|
```yaml
|
|
538
542
|
Kaizo/NextInNonVoidEnumerable:
|
|
539
543
|
AllowedMethods:
|
|
540
|
-
- reduce #
|
|
541
|
-
AllowedPatterns: []
|
|
544
|
+
- reduce # permit `next <acc>` guards in reduce/inject
|
|
542
545
|
```
|
|
543
546
|
|
|
544
547
|
## Plural names for collections
|
|
545
548
|
|
|
546
549
|
`Kaizo/PluralCollectionName` flags a method that hands back an array under a
|
|
547
|
-
singular name
|
|
548
|
-
|
|
550
|
+
singular name — `users` tells the caller what they are getting; `user`
|
|
551
|
+
actively misleads them.
|
|
549
552
|
|
|
550
553
|
```ruby
|
|
551
554
|
# bad
|
|
@@ -557,16 +560,9 @@ end
|
|
|
557
560
|
def users
|
|
558
561
|
[first_match, second_match]
|
|
559
562
|
end
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
Ruby has no return types, so "returns an array" is a heuristic — and this cop
|
|
563
|
-
deliberately errs toward silence. A method is flagged only when **every** value
|
|
564
|
-
it can return is unambiguously an array: an array literal, or a call to a method
|
|
565
|
-
in `ArrayMethods` whose result is an `Array` whatever its receiver. A single
|
|
566
|
-
branch returning something else is enough to leave the method alone:
|
|
567
563
|
|
|
568
|
-
|
|
569
|
-
#
|
|
564
|
+
# good — not confidently a collection (one branch is not an array), so the
|
|
565
|
+
# cop deliberately errs toward silence
|
|
570
566
|
def user
|
|
571
567
|
return nil if missing?
|
|
572
568
|
|
|
@@ -574,23 +570,24 @@ def user
|
|
|
574
570
|
end
|
|
575
571
|
```
|
|
576
572
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
573
|
+
A method is flagged only when every value it can return is unambiguously an
|
|
574
|
+
array: a literal, or a call to an `ArrayMethods` entry. `select`/`reject` are
|
|
575
|
+
absent from that default on purpose — on a `Hash` they return a `Hash`. A
|
|
576
|
+
name counts as plural when it ends in `s` or appears in `IrregularPlurals`;
|
|
577
|
+
predicates, writers, operators, and `initialize` are exempt.
|
|
582
578
|
|
|
583
579
|
```yaml
|
|
584
580
|
Kaizo/PluralCollectionName:
|
|
585
|
-
|
|
581
|
+
inherit_mode:
|
|
582
|
+
merge:
|
|
583
|
+
- ArrayMethods
|
|
584
|
+
- IrregularPlurals
|
|
585
|
+
ArrayMethods:
|
|
586
|
+
- fetch_all # your own collection-returning helper
|
|
586
587
|
IrregularPlurals:
|
|
587
|
-
-
|
|
588
|
-
- children
|
|
588
|
+
- alumni # plural without a trailing `s`
|
|
589
589
|
```
|
|
590
590
|
|
|
591
|
-
As with the other cops there is **no autocorrection** — only the author knows
|
|
592
|
-
the right plural.
|
|
593
|
-
|
|
594
591
|
## Development
|
|
595
592
|
|
|
596
593
|
```bash
|