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.
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 by
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
- A long argument list is a smell: it usually means a method is juggling loose
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 | Bounds | Counts |
32
- |-----|--------|--------|
33
- | `Kaizo/PositionalArguments` | positional params | `arg`, `optarg` |
34
- | `Kaizo/KeywordArguments` | keyword params | `kwarg`, `kwoptarg` |
35
- | `Kaizo/TotalArguments` | positional + keyword | all of the above |
36
-
37
- Each cop has a `Max` option. All three check `def`, `def self.`,
38
- `define_method`, and `define_singleton_method`.
39
-
40
- The three cops are independent and complementary enable whichever dimensions
41
- you want to bound. `TotalArguments` alone is a single global cap; pairing
42
- `PositionalArguments` with `KeywordArguments` bounds each kind separately (so you
43
- can, for instance, forbid positional arguments while allowing a couple of keyword
44
- ones); running `TotalArguments` alongside them also catches methods that stay
45
- under each per-kind limit but exceed the total. A method that breaks more than
46
- one bound is reported once per cop it violates — by design — so enable the
47
- smallest set that expresses your rule.
48
-
49
- Beyond argument counts, the gem ships **`Kaizo/AgentNounClassName`**, which
50
- flags classes named after what they *do* (see [Class naming](#class-naming)),
51
- **`Kaizo/NestedMethodCalls`**, which flags calls buried too deeply in other
52
- calls' arguments (see [Nested method calls](#nested-method-calls)),
53
- **`Kaizo/SpecComment`**, which flags comments in spec files (see
54
- [Comments in specs](#comments-in-specs)), **`Kaizo/SpecDescriptionProse`**,
55
- which requires `it`/`context` descriptions to read as one-behavior prose (see
56
- [Spec description prose](#spec-description-prose)),
57
- **`Kaizo/FileUtilsInclusion`**, which asks you to `include`/`extend` `FileUtils`
58
- once its methods are used more than once (see
59
- [Including FileUtils](#including-fileutils)), **`Kaizo/PreferPathname`**, which
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
- (Requires RuboCop 1.72.2+ for the `lint_roller` plugin API.)
84
+ Requires RuboCop 1.72.2+ for the `lint_roller` plugin API.
83
85
 
84
- ## Configuration
86
+ ## Argument counts
85
87
 
86
- The defaults are deliberately strict — **at most one positional and one keyword
87
- argument** to apply maximum pressure toward modeling. Loosen them if that is
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
- Setting `Max: 0` forbids a kind of argument entirely — for example, banning
100
- positional arguments so that every parameter must be passed by keyword:
100
+ All three check `def`, `def self.`, `define_method`, and
101
+ `define_singleton_method`. Required and optional parameters count alike:
101
102
 
102
- ```yaml
103
- Kaizo/PositionalArguments:
104
- Max: 0 # every argument must be passed by keyword
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
- ### What is counted
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
- Only **named, individual** parameters count toward the limits:
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
- - Positional: required (`a`) and optional (`a = 1`).
112
- - Keyword: required (`a:`) and optional (`a: 1`).
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
- The variadic collectors `*rest`, `**keyword_rest`, `&block`, and `...` are **not**
115
- counted — they are single tokens, not a list of primitives.
138
+ Police tests like everything else by clearing the exclusion:
116
139
 
117
- ### Exemptions
140
+ ```yaml
141
+ Kaizo/KeywordArguments:
142
+ Exclude: [] # count keywords in tests too
143
+ Kaizo/TotalArguments:
144
+ Exclude: []
145
+ ```
118
146
 
119
- The `initialize` of a `Struct.new` or `Data.define` block is exempt, because its
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
- # not flagged
125
- Data.define(:width, :height, :depth, :weight) do
126
- def initialize(width:, height:, depth:, weight:)
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
- **Operator methods** are exempt too. The arity of `[]=` is fixed by Ruby's syntax
133
- an index (or indices) plus the value being assigned so there is no object to
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
- This covers `def`, `def self.`, and `define_method(:[]=)`. A `define_method` whose
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
- There is intentionally **no autocorrection**: the fix is a design decision (what
151
- object should these arguments become?), and that belongs to a human.
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
- ## Relationship to `Metrics/ParameterLists`
177
+ ### `define_method` edge cases
154
178
 
155
- Core RuboCop's `Metrics/ParameterLists` enforces a single maximum on the whole
156
- parameter list. `kaizo` is more granular: it bounds positional and
157
- keyword arguments separately (and together), and is framed around domain
158
- modeling rather than method complexity. Use whichever fits; they can coexist.
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
- For the full rationale why the counting is implemented here rather than reusing
161
- or configuring `Metrics/ParameterLists`, with reproducible evidence see
162
- [docs/why-not-metrics-parameterlists.md](docs/why-not-metrics-parameterlists.md).
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
- ## Known limitations
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
- - **The proc/lambda form of `define_method` is not inspected.** Only the block
167
- form (`define_method(:foo) { |a, b| }`) is checked. When the body is supplied
168
- as a callable `define_method(:foo, ->(a, b) {})` or
169
- `define_method(:foo, captured_method)` it is left alone, because the argument
170
- may be any object that responds to `call` and is not statically countable in
171
- the general case.
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. A class whose name ends in `er`/`or`
181
- (`OrderManager`, `PaymentProcessor`, `RequestHandler`), or in a configured
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
- ### Tuning the lists
200
-
201
- Two suffix lists drive it, both fully configurable and matched against the last
202
- segment of a namespaced name (`Billing::InvoiceBuilder` is checked as
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
- Extend either list without restating the default using RuboCop's `inherit_mode`:
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
- - Ledger # OrderLedger now passes
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 method calls nested too deeply in **argument**
230
- positions — `foo(SomeClass.new(another("bar").chain))` on the principle that the
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
- # bad - named, but the name says nothing
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 - a single nested call is fine
253
+ # good a single nested call is fine at the default Max
252
254
  puts compute(value)
253
255
  ```
254
256
 
255
- Depth is bounded by `Max` (default `1` one nested call is allowed). Only nesting
256
- through **arguments** is counted; a *receiver chain* such as
257
- `user.account.owner.name` is a separate concern (a dedicated chaining cop is
258
- planned). Operator methods (`a + b`, `arr[i]`) never count, block bodies are not
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. A comment in a spec is almost
274
- always a sign that the spec is doing the job of its own description: if you need a
275
- sentence to explain what an example sets up or asserts, that sentence usually
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
- By default only `*_spec.rb` files are inspected. Magic comments
295
- (`# frozen_string_literal: true`, `# encoding: …`), RuboCop directives
296
- (any `# rubocop:` comment), and shebangs are never flagged. Like
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 RSpec `it`/`context` descriptions to read
329
- as one-behavior prose specifications. Every rule is **structural** — it fires
330
- only when the wording signals that one example is really more than one, or that
331
- the assertion is leaking into the name.
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
- The defaults are deliberately curated, not exhaustive: `for` is dropped from the
363
- conjunctions (it is a preposition in nearly every description), and homographs
364
- like `even`/`given`/`regardless` are left out (they collide with `even numbers`
365
- and the like) add them via `Conjunctions` if you want them. Pure **wording**
366
- preferences that don't change structure (e.g. `should` vs a present-tense verb)
367
- are out of scope rubocop-rspec's `RSpec/ExampleWording` already covers those.
368
- There is no autocorrection: splitting an example, or extracting a condition into
369
- a `context`, is a modelling decision for a human.
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 repeated qualified `FileUtils.` calls within a
374
- class or module: once you reach for `FileUtils` more than once, `include` it (for
375
- instance-level use) or `extend` it (for class/singleton-level use) and call its
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
- The class or module is reported once. A single qualified call is left alone, a
399
- namespace that already mixes `FileUtils` in is not flagged, and nested classes
400
- and modules are counted on their own (one call in an outer class and one in a
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 calls to `File` class methods that have a `Pathname`
407
- instance-method equivalent — `File.read`, `File.exist?`, `File.join`,
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 own
424
- public instance methods (so `File.new`, and `File.path` whose `Pathname#path`
425
- equivalent is protected, are left alone).
426
- The cop runs on `**/*.rb` and skips `exe/**/*` and `bin/**/*` by default
427
- executables often work with raw path strings which you can adjust with the
428
- standard `Include`/`Exclude` options:
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
- Because the ban is broad, a few equivalents are not drop-in replacements:
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
- As with most of the cops here, there is **no autocorrection** — rewriting
446
- `File.read(path)` as `Pathname(path).read` changes the receiver and is a call for
447
- a human.
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 a `rescue` or `ensure` directly to the `def` Ruby's "implicit
453
- begin". It is the inverse of core's `Style/RedundantBegin`. An explicit `begin`
454
- names the guarded region and keeps it bounded: it marks exactly what the
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 method definitions are not
477
- flagged. Unlike the other cops, this one **does autocorrect** (`rubocop -a`)
478
- wrapping a body in `begin`/`end` is a mechanical fix, not a design decision. The
479
- correction is skipped when the body does not sit on its own lines between `def`
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, loading this
485
- plugin **disables it** by default — otherwise the two autocorrections would loop
486
- forever, each undoing the other. Re-enable it explicitly in your `.rubocop.yml`
487
- if you would rather not require explicit begins:
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 # opt back out of Kaizo/ExplicitBegin
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`, `reduce`,
498
- `sum`, `group_by`, the `*_by` methods, the `any?`/`all?`/`none?`/`one?`
499
- predicates, and so on — where `next` is being used as control-flow-as-value.
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 - `next <value>` counts too
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 - void iteration method; `next` just skips the iteration
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 - say what you mean
533
+ # good say what you mean
521
534
  array.filter_map { |item| transform(item) unless skip?(item) }
522
535
  ```
523
536
 
524
- Only *void* iteration methods those whose block return value is ignored
525
- (`each`, `each_with_index`, `each_slice`, `each_with_object`, `reverse_each`,
526
- `cycle`, …) are meant to use `next`, which is why they are absent from the
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 # allow `next <acc>` guards in reduce/inject
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. The plural does the documenting for free `users` tells the
548
- caller what they are getting; `user` actively misleads them.
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
- ```ruby
569
- # good - not confidently a collection, so not flagged
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
- `select` and `reject` are absent from the default `ArrayMethods` on purpose: on a
578
- `Hash` they return a `Hash`, and including them would turn this into a
579
- false-positive mill. A name counts as plural when it ends in `s` or appears in
580
- `IrregularPlurals`. Predicate (`?`), writer (`=`), and operator methods are
581
- exempt, as is `initialize`.
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
- AllowedMethods: []
581
+ inherit_mode:
582
+ merge:
583
+ - ArrayMethods
584
+ - IrregularPlurals
585
+ ArrayMethods:
586
+ - fetch_all # your own collection-returning helper
586
587
  IrregularPlurals:
587
- - people # plural without a trailing `s`
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