kaizo 0.7.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 +119 -0
- data/README.md +332 -259
- data/config/default.yml +112 -8
- data/lib/kaizo/version.rb +1 -1
- data/lib/rubocop/cop/kaizo/agent_noun_class_name.rb +24 -1
- data/lib/rubocop/cop/kaizo/argument_counting.rb +46 -6
- data/lib/rubocop/cop/kaizo/explicit_begin.rb +9 -0
- data/lib/rubocop/cop/kaizo/file_utils_inclusion.rb +6 -1
- data/lib/rubocop/cop/kaizo/keyword_arguments.rb +22 -0
- data/lib/rubocop/cop/kaizo/nested_method_calls.rb +20 -6
- data/lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb +13 -2
- data/lib/rubocop/cop/kaizo/plural_collection_name.rb +139 -0
- 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 +62 -22
- 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 +3 -0
- metadata +5 -2
data/README.md
CHANGED
|
@@ -1,198 +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
|
|
157
|
+
|
|
158
|
+
# good — operator arity is fixed by Ruby's syntax; there is no object to
|
|
159
|
+
# extract (`[]`, `<=>`, `+`, `<<`, and the rest of the family likewise)
|
|
160
|
+
def []=(row, column, value)
|
|
161
|
+
@cells[row][column] = value
|
|
162
|
+
end
|
|
130
163
|
```
|
|
131
164
|
|
|
132
|
-
|
|
133
|
-
object should these arguments become?), and that belongs to a human.
|
|
165
|
+
Beyond the structural exemptions, exempt methods by name or pattern:
|
|
134
166
|
|
|
135
|
-
|
|
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
|
+
```
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
parameter list. `kaizo` is more granular: it bounds positional and
|
|
139
|
-
keyword arguments separately (and together), and is framed around domain
|
|
140
|
-
modeling rather than method complexity. Use whichever fits; they can coexist.
|
|
177
|
+
### `define_method` edge cases
|
|
141
178
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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] })
|
|
183
|
+
|
|
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 }
|
|
187
|
+
|
|
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
|
+
```
|
|
145
192
|
|
|
146
|
-
##
|
|
193
|
+
## Relationship to `Metrics/ParameterLists`
|
|
147
194
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
may be any object that responds to `call` and is not statically countable in
|
|
153
|
-
the general case.
|
|
154
|
-
- **Numbered and `it` block parameters count as zero.**
|
|
155
|
-
`define_method(:squared) { _1 * _1 }` is treated as taking no arguments:
|
|
156
|
-
implicit block parameters are not part of a declared signature, which is what
|
|
157
|
-
these cops measure. Spell the parameters out if you want them counted.
|
|
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).
|
|
158
199
|
|
|
159
200
|
## Class naming
|
|
160
201
|
|
|
161
202
|
`Kaizo/AgentNounClassName` flags classes named as agent nouns — "doers" —
|
|
162
|
-
rather than the domain concepts they model
|
|
163
|
-
|
|
164
|
-
forbidden suffix like `Service`, usually means procedural behavior that wants a
|
|
165
|
-
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`.
|
|
166
205
|
|
|
167
206
|
```ruby
|
|
168
|
-
# bad
|
|
207
|
+
# bad — an -er name describes behavior, not a concept
|
|
169
208
|
class PaymentProcessor
|
|
170
209
|
end
|
|
171
210
|
|
|
211
|
+
# bad — Struct/Data/Class constant assignments are checked too
|
|
212
|
+
RequestHandler = Data.define(:request)
|
|
213
|
+
|
|
172
214
|
# good
|
|
173
215
|
class Payment
|
|
174
216
|
end
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
It checks `class` definitions and `Struct.new` / `Data.define` / `Class.new`
|
|
178
|
-
constant assignments. Like the argument-count cops, there is **no autocorrection** — a
|
|
179
|
-
rename is a design decision.
|
|
180
|
-
|
|
181
|
-
### Tuning the lists
|
|
182
217
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- **`AllowedSuffixes`** — exempt these. Matched as a suffix, so `Controller`
|
|
188
|
-
clears `Controller` and `UsersController` alike. Ships with a broad default of
|
|
189
|
-
legitimate `-er`/`-or` words — domain nouns (`Order`, `User`, `Number`,
|
|
190
|
-
`Error`) and framework terms (`Controller`, `Serializer`, `Adapter`).
|
|
191
|
-
- **`ForbiddenSuffixes`** — always flag these, even when they don't end in
|
|
192
|
-
`-er`/`-or` (default: `Service`, `Util`, `Utils`). This list **wins** over
|
|
193
|
-
`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
|
+
```
|
|
194
222
|
|
|
195
|
-
|
|
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:
|
|
196
227
|
|
|
197
228
|
```yaml
|
|
198
229
|
Kaizo/AgentNounClassName:
|
|
@@ -201,36 +232,32 @@ Kaizo/AgentNounClassName:
|
|
|
201
232
|
- AllowedSuffixes
|
|
202
233
|
- ForbiddenSuffixes
|
|
203
234
|
AllowedSuffixes:
|
|
204
|
-
-
|
|
235
|
+
- Voucher # PaymentVoucher now passes
|
|
205
236
|
ForbiddenSuffixes:
|
|
206
237
|
- Server # ApiServer now flagged, despite the default allowance
|
|
207
238
|
```
|
|
208
239
|
|
|
209
240
|
## Nested method calls
|
|
210
241
|
|
|
211
|
-
`Kaizo/NestedMethodCalls` flags
|
|
212
|
-
positions —
|
|
213
|
-
intermediate results want names. Reaching for the right name (or extracting a
|
|
214
|
-
method) almost always reads better, and is easier to debug, than peeling
|
|
215
|
-
parentheses apart.
|
|
242
|
+
`Kaizo/NestedMethodCalls` flags calls nested too deeply in **argument**
|
|
243
|
+
positions — intermediate results want names.
|
|
216
244
|
|
|
217
245
|
```ruby
|
|
218
246
|
# bad
|
|
219
247
|
wrap(parse(read(io)))
|
|
220
248
|
|
|
221
|
-
# good
|
|
222
|
-
|
|
223
|
-
wrap(
|
|
249
|
+
# good — the name turns the step into its own documentation
|
|
250
|
+
parsed_config = parse(read(io))
|
|
251
|
+
wrap(parsed_config)
|
|
224
252
|
|
|
225
|
-
# good
|
|
253
|
+
# good — a single nested call is fine at the default Max
|
|
226
254
|
puts compute(value)
|
|
227
255
|
```
|
|
228
256
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`user.account.owner
|
|
232
|
-
|
|
233
|
-
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.
|
|
234
261
|
|
|
235
262
|
```yaml
|
|
236
263
|
Kaizo/NestedMethodCalls:
|
|
@@ -239,16 +266,11 @@ Kaizo/NestedMethodCalls:
|
|
|
239
266
|
- expect # e.g. don't count RSpec's expect(...) wrapper
|
|
240
267
|
```
|
|
241
268
|
|
|
242
|
-
Like the other cops, there is **no autocorrection** — choosing the intermediate
|
|
243
|
-
name is a design decision.
|
|
244
|
-
|
|
245
269
|
## Comments in specs
|
|
246
270
|
|
|
247
|
-
`Kaizo/SpecComment` flags comments in spec files
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
wants to be a `context`/`it` description, a clearer example name, or another
|
|
251
|
-
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.
|
|
252
274
|
|
|
253
275
|
```ruby
|
|
254
276
|
# bad
|
|
@@ -265,17 +287,9 @@ it 'permits an admin to see everything' do
|
|
|
265
287
|
end
|
|
266
288
|
```
|
|
267
289
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
the other cops, there is **no autocorrection** — turning an explanation into a
|
|
272
|
-
spec is a design decision.
|
|
273
|
-
|
|
274
|
-
### Scope and escape hatches
|
|
275
|
-
|
|
276
|
-
The cop is scoped through its `Include`, so broaden it to cover support files or a
|
|
277
|
-
Minitest suite (using `inherit_mode: merge` to add to the default rather than
|
|
278
|
-
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.
|
|
279
293
|
|
|
280
294
|
```yaml
|
|
281
295
|
Kaizo/SpecComment:
|
|
@@ -285,69 +299,94 @@ Kaizo/SpecComment:
|
|
|
285
299
|
Include:
|
|
286
300
|
- '**/spec/**/*' # spec_helper, support/, factories
|
|
287
301
|
- '**/*_test.rb' # Minitest / Test::Unit
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
Permit specific comments with `AllowedPatterns` — regexps matched against the
|
|
291
|
-
full comment text, leading `#` included:
|
|
292
|
-
|
|
293
|
-
```yaml
|
|
294
|
-
Kaizo/SpecComment:
|
|
302
|
+
Exclude: [] # police spec/helpers and spec/support too
|
|
295
303
|
AllowedPatterns:
|
|
296
|
-
- '\A#\s*@rbs' # rbs-inline type annotations
|
|
297
|
-
- 'noqa'
|
|
304
|
+
- '\A#\s*@rbs' # permit rbs-inline type annotations
|
|
298
305
|
```
|
|
299
306
|
|
|
300
307
|
## Spec description prose
|
|
301
308
|
|
|
302
|
-
`Kaizo/SpecDescriptionProse` requires
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
An `it`/`specify`/`example` description must not contain:
|
|
308
|
-
|
|
309
|
-
- a **comma** — a list is several behaviors;
|
|
310
|
-
- a **conjunction** (`and`, `or`, `so`, `when`, `if`, `unless`, … — the
|
|
311
|
-
`Conjunctions` list) — joined clauses are separate examples, and a condition
|
|
312
|
-
belongs in a `context`;
|
|
313
|
-
- **code** — `_ : # = { } ! [ ]`, a backtick, or a nested quoted literal;
|
|
314
|
-
a description is prose, not identifiers or wire values.
|
|
315
|
-
|
|
316
|
-
A `context` description must not contain code, and must open with a word from
|
|
317
|
-
`ContextPrefixes` (`when`/`with`/`without`/`after`). `describe` strings name the
|
|
318
|
-
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.
|
|
319
313
|
|
|
320
314
|
```ruby
|
|
321
315
|
# bad
|
|
322
|
-
it "renders the name, image, and flag"
|
|
323
|
-
it "omits the key when the role is unset"
|
|
324
|
-
it "renders the :cpu member"
|
|
325
|
-
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
|
|
326
320
|
end
|
|
327
321
|
|
|
328
322
|
# good
|
|
329
323
|
it "renders the name"
|
|
330
324
|
it "renders the cpu member"
|
|
325
|
+
it "raises Timeout::Error"
|
|
331
326
|
context "when the role is unset" do
|
|
332
327
|
it "omits the key"
|
|
333
328
|
end
|
|
334
329
|
```
|
|
335
330
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
```
|
|
344
384
|
|
|
345
385
|
## Including FileUtils
|
|
346
386
|
|
|
347
|
-
`Kaizo/FileUtilsInclusion` flags
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
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.
|
|
351
390
|
|
|
352
391
|
```ruby
|
|
353
392
|
# bad
|
|
@@ -369,17 +408,13 @@ class Backup
|
|
|
369
408
|
end
|
|
370
409
|
```
|
|
371
410
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
nested class do not add up). As with most of the cops here, there is **no
|
|
376
|
-
autocorrection** — whether to `include` or `extend`, and where the mixin belongs,
|
|
377
|
-
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
|
+
|
|
378
414
|
## Prefer Pathname
|
|
379
415
|
|
|
380
|
-
`Kaizo/PreferPathname` flags
|
|
381
|
-
instance-method equivalent — `
|
|
382
|
-
`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
|
|
383
418
|
on it reads better than threading a string through `File`.
|
|
384
419
|
|
|
385
420
|
```ruby
|
|
@@ -394,12 +429,12 @@ path.exist?
|
|
|
394
429
|
dir.join(name)
|
|
395
430
|
```
|
|
396
431
|
|
|
397
|
-
The banned set is the intersection of `File`'s class methods and `Pathname`'s
|
|
398
|
-
public instance methods
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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:
|
|
403
438
|
|
|
404
439
|
```yaml
|
|
405
440
|
Kaizo/PreferPathname:
|
|
@@ -409,25 +444,29 @@ Kaizo/PreferPathname:
|
|
|
409
444
|
- 'db/**/*' # add your own
|
|
410
445
|
```
|
|
411
446
|
|
|
412
|
-
|
|
413
|
-
`Pathname#join` treats an absolute segment as a reset (`Pathname("a").join("/b")`
|
|
414
|
-
is `/b`, where `File.join("a", "/b")` is `a/b`), `Pathname#chmod`/`chown`/`utime`
|
|
415
|
-
act on the single receiver (where `File.chmod` is variadic over many paths), and
|
|
416
|
-
`Pathname#split`/`rename` differ in return type and arity. The cop only points;
|
|
417
|
-
mind those differences when you rewrite — part of why it does not autocorrect.
|
|
447
|
+
## Temp files
|
|
418
448
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
```
|
|
422
463
|
|
|
423
464
|
## Explicit begin
|
|
424
465
|
|
|
425
466
|
`Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
|
|
426
|
-
body attaches
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`rescue`/`ensure` covers, so the method can grow other statements without
|
|
430
|
-
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.
|
|
431
470
|
|
|
432
471
|
```ruby
|
|
433
472
|
# bad
|
|
@@ -447,30 +486,30 @@ def foo
|
|
|
447
486
|
end
|
|
448
487
|
```
|
|
449
488
|
|
|
450
|
-
Modifier rescues (`foo rescue nil`) and endless
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
and `end` (a single-line definition, say), or contains a heredoc or other
|
|
455
|
-
multiline string, symbol, or regexp literal, where re-indenting could change
|
|
456
|
-
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.
|
|
457
493
|
|
|
458
|
-
Because `Style/RedundantBegin` enforces the exact opposite style,
|
|
459
|
-
plugin
|
|
460
|
-
forever
|
|
461
|
-
|
|
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:
|
|
462
499
|
|
|
463
500
|
```yaml
|
|
501
|
+
Kaizo/ExplicitBegin:
|
|
502
|
+
Enabled: false # opt out of explicit begins
|
|
464
503
|
Style/RedundantBegin:
|
|
465
|
-
Enabled: true #
|
|
504
|
+
Enabled: true # optional: enforce the inverse style instead
|
|
466
505
|
```
|
|
467
506
|
|
|
468
507
|
## Next in value-returning blocks
|
|
469
508
|
|
|
470
509
|
`Kaizo/NextInNonVoidEnumerable` flags `next` inside the block of a
|
|
471
|
-
value-returning `Enumerable` method — `map`, `select`, `filter_map`,
|
|
472
|
-
`
|
|
473
|
-
|
|
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.
|
|
474
513
|
|
|
475
514
|
```ruby
|
|
476
515
|
# bad
|
|
@@ -479,40 +518,74 @@ array.map do |item|
|
|
|
479
518
|
transform(item)
|
|
480
519
|
end
|
|
481
520
|
|
|
482
|
-
# bad
|
|
521
|
+
# bad — `next <value>` counts too
|
|
483
522
|
array.reduce(0) do |sum, item|
|
|
484
523
|
next sum if skip?(item)
|
|
485
524
|
sum + item
|
|
486
525
|
end
|
|
487
526
|
|
|
488
|
-
# good
|
|
527
|
+
# good — a void iteration method; `next` just skips the iteration
|
|
489
528
|
array.each do |item|
|
|
490
529
|
next if skip?(item)
|
|
491
530
|
process(item)
|
|
492
531
|
end
|
|
493
532
|
|
|
494
|
-
# good
|
|
533
|
+
# good — say what you mean
|
|
495
534
|
array.filter_map { |item| transform(item) unless skip?(item) }
|
|
496
535
|
```
|
|
497
536
|
|
|
498
|
-
|
|
499
|
-
(`
|
|
500
|
-
|
|
501
|
-
flagged set. Non-`Enumerable` looping constructs (`loop`, `Integer#times`,
|
|
502
|
-
`while`) are likewise never flagged. A `next` that binds to a nested block or
|
|
503
|
-
loop is attributed to that inner scope, so an inner `each { next }` or
|
|
504
|
-
`while … next … end` does not flag an outer `map`.
|
|
505
|
-
|
|
506
|
-
As with most of the cops here, there is **no autocorrection** — the right fix
|
|
507
|
-
depends on intent (a guard clause might become a ternary, a `select`/`reject`, a
|
|
508
|
-
`filter_map`, or a restructured block). Exempt specific methods with
|
|
509
|
-
`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.
|
|
510
540
|
|
|
511
541
|
```yaml
|
|
512
542
|
Kaizo/NextInNonVoidEnumerable:
|
|
513
543
|
AllowedMethods:
|
|
514
|
-
- reduce #
|
|
515
|
-
|
|
544
|
+
- reduce # permit `next <acc>` guards in reduce/inject
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Plural names for collections
|
|
548
|
+
|
|
549
|
+
`Kaizo/PluralCollectionName` flags a method that hands back an array under a
|
|
550
|
+
singular name — `users` tells the caller what they are getting; `user`
|
|
551
|
+
actively misleads them.
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# bad
|
|
555
|
+
def user
|
|
556
|
+
[first_match, second_match]
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# good
|
|
560
|
+
def users
|
|
561
|
+
[first_match, second_match]
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# good — not confidently a collection (one branch is not an array), so the
|
|
565
|
+
# cop deliberately errs toward silence
|
|
566
|
+
def user
|
|
567
|
+
return nil if missing?
|
|
568
|
+
|
|
569
|
+
[first_match, second_match]
|
|
570
|
+
end
|
|
571
|
+
```
|
|
572
|
+
|
|
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.
|
|
578
|
+
|
|
579
|
+
```yaml
|
|
580
|
+
Kaizo/PluralCollectionName:
|
|
581
|
+
inherit_mode:
|
|
582
|
+
merge:
|
|
583
|
+
- ArrayMethods
|
|
584
|
+
- IrregularPlurals
|
|
585
|
+
ArrayMethods:
|
|
586
|
+
- fetch_all # your own collection-returning helper
|
|
587
|
+
IrregularPlurals:
|
|
588
|
+
- alumni # plural without a trailing `s`
|
|
516
589
|
```
|
|
517
590
|
|
|
518
591
|
## Development
|