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.
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 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
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
- There is intentionally **no autocorrection**: the fix is a design decision (what
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
- ## Relationship to `Metrics/ParameterLists`
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
- Core RuboCop's `Metrics/ParameterLists` enforces a single maximum on the whole
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
- For the full rationale — why the counting is implemented here rather than reusing
143
- or configuring `Metrics/ParameterLists`, with reproducible evidence see
144
- [docs/why-not-metrics-parameterlists.md](docs/why-not-metrics-parameterlists.md).
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
- ## Known limitations
193
+ ## Relationship to `Metrics/ParameterLists`
147
194
 
148
- - **The proc/lambda form of `define_method` is not inspected.** Only the block
149
- form (`define_method(:foo) { |a, b| }`) is checked. When the body is supplied
150
- as a callable `define_method(:foo, ->(a, b) {})` or
151
- `define_method(:foo, captured_method)` — it is left alone, because the argument
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. A class whose name ends in `er`/`or`
163
- (`OrderManager`, `PaymentProcessor`, `RequestHandler`), or in a configured
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
- Two suffix lists drive it, both fully configurable and matched against the last
184
- segment of a namespaced name (`Billing::InvoiceBuilder` is checked as
185
- `InvoiceBuilder`):
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
- 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:
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
- - Ledger # OrderLedger now passes
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 method calls nested too deeply in **argument**
212
- positions — `foo(SomeClass.new(another("bar").chain))` on the principle that the
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 - name the intermediate result
222
- parsed = parse(read(io))
223
- wrap(parsed)
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 - a single nested call is fine
253
+ # good a single nested call is fine at the default Max
226
254
  puts compute(value)
227
255
  ```
228
256
 
229
- Depth is bounded by `Max` (default `1` one nested call is allowed). Only nesting
230
- through **arguments** is counted; a *receiver chain* such as
231
- `user.account.owner.name` is a separate concern (a dedicated chaining cop is
232
- planned). Operator methods (`a + b`, `arr[i]`) never count, block bodies are not
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. A comment in a spec is almost
248
- always a sign that the spec is doing the job of its own description: if you need a
249
- sentence to explain what an example sets up or asserts, that sentence usually
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
- By default only `*_spec.rb` files are inspected. Magic comments
269
- (`# frozen_string_literal: true`, `# encoding: …`), RuboCop directives
270
- (any `# rubocop:` comment), and shebangs are never flagged. Like
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 RSpec `it`/`context` descriptions to read
303
- as one-behavior prose specifications. Every rule is **structural** — it fires
304
- only when the wording signals that one example is really more than one, or that
305
- the assertion is leaking into the name.
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
- The defaults are deliberately curated, not exhaustive: `for` is dropped from the
337
- conjunctions (it is a preposition in nearly every description), and homographs
338
- like `even`/`given`/`regardless` are left out (they collide with `even numbers`
339
- and the like) add them via `Conjunctions` if you want them. Pure **wording**
340
- preferences that don't change structure (e.g. `should` vs a present-tense verb)
341
- are out of scope rubocop-rspec's `RSpec/ExampleWording` already covers those.
342
- There is no autocorrection: splitting an example, or extracting a condition into
343
- 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
+ ```
344
384
 
345
385
  ## Including FileUtils
346
386
 
347
- `Kaizo/FileUtilsInclusion` flags repeated qualified `FileUtils.` calls within a
348
- class or module: once you reach for `FileUtils` more than once, `include` it (for
349
- instance-level use) or `extend` it (for class/singleton-level use) and call its
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
- The class or module is reported once. A single qualified call is left alone, a
373
- namespace that already mixes `FileUtils` in is not flagged, and nested classes
374
- and modules are counted on their own (one call in an outer class and one in a
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 calls to `File` class methods that have a `Pathname`
381
- instance-method equivalent — `File.read`, `File.exist?`, `File.join`,
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 own
398
- public instance methods (so `File.new`, and `File.path` whose `Pathname#path`
399
- equivalent is protected, are left alone).
400
- The cop runs on `**/*.rb` and skips `exe/**/*` and `bin/**/*` by default
401
- executables often work with raw path strings which you can adjust with the
402
- 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:
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
- Because the ban is broad, a few equivalents are not drop-in replacements:
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
- As with most of the cops here, there is **no autocorrection** — rewriting
420
- `File.read(path)` as `Pathname(path).read` changes the receiver and is a call for
421
- 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
+ ```
422
463
 
423
464
  ## Explicit begin
424
465
 
425
466
  `Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
426
- body attaches a `rescue` or `ensure` directly to the `def` Ruby's "implicit
427
- begin". It is the inverse of core's `Style/RedundantBegin`. An explicit `begin`
428
- names the guarded region and keeps it bounded: it marks exactly what the
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 method definitions are not
451
- flagged. Unlike the other cops, this one **does autocorrect** (`rubocop -a`)
452
- wrapping a body in `begin`/`end` is a mechanical fix, not a design decision. The
453
- correction is skipped when the body does not sit on its own lines between `def`
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, loading this
459
- plugin **disables it** by default — otherwise the two autocorrections would loop
460
- forever, each undoing the other. Re-enable it explicitly in your `.rubocop.yml`
461
- 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:
462
499
 
463
500
  ```yaml
501
+ Kaizo/ExplicitBegin:
502
+ Enabled: false # opt out of explicit begins
464
503
  Style/RedundantBegin:
465
- Enabled: true # opt back out of Kaizo/ExplicitBegin
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`, `reduce`,
472
- `sum`, `group_by`, the `*_by` methods, the `any?`/`all?`/`none?`/`one?`
473
- 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.
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 - `next <value>` counts too
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 - void iteration method; `next` just skips the iteration
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 - say what you mean
533
+ # good say what you mean
495
534
  array.filter_map { |item| transform(item) unless skip?(item) }
496
535
  ```
497
536
 
498
- Only *void* iteration methods those whose block return value is ignored
499
- (`each`, `each_with_index`, `each_slice`, `each_with_object`, `reverse_each`,
500
- `cycle`, …) are meant to use `next`, which is why they are absent from the
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 # allow `next <acc>` guards in reduce/inject
515
- AllowedPatterns: []
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