kaizo 0.7.0
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 +7 -0
- data/CHANGELOG.md +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +529 -0
- data/config/default.yml +210 -0
- data/lib/kaizo/plugin.rb +27 -0
- data/lib/kaizo/version.rb +3 -0
- data/lib/kaizo.rb +15 -0
- data/lib/rubocop/cop/kaizo/agent_noun_class_name.rb +76 -0
- data/lib/rubocop/cop/kaizo/argument_counting.rb +74 -0
- data/lib/rubocop/cop/kaizo/explicit_begin.rb +119 -0
- data/lib/rubocop/cop/kaizo/file_utils_inclusion.rb +92 -0
- data/lib/rubocop/cop/kaizo/keyword_arguments.rb +35 -0
- data/lib/rubocop/cop/kaizo/nested_method_calls.rb +130 -0
- data/lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb +134 -0
- data/lib/rubocop/cop/kaizo/positional_arguments.rb +35 -0
- data/lib/rubocop/cop/kaizo/prefer_pathname.rb +63 -0
- data/lib/rubocop/cop/kaizo/spec_comment.rb +75 -0
- data/lib/rubocop/cop/kaizo/spec_description_prose.rb +136 -0
- data/lib/rubocop/cop/kaizo/total_arguments.rb +39 -0
- data/lib/rubocop/cop/kaizo_cops.rb +12 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7a167afcda94e477c2fd351f0f88c4c4f69b4adb987e0e74a59f50c516aa6080
|
|
4
|
+
data.tar.gz: f002c0b1464262edc8eb988f046b935fb9347d2258585c143acc6dd46911f86b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 41ec7ca0e3f99dc13c761540b9ac7c06bd66f1cbcdecf776db8f1dd4dcd0c412008ae70f6ac73e6d9295662a27d70b3c2b2973930674ea7229fc1d173b4bb33e
|
|
7
|
+
data.tar.gz: f3ea240a663c70959adf03f95c5b49452ae3ae71a93818b8dcdedcf8bf1b72f10acd9d1db95494e12f7ee592a3162b8590a5d99603caca710126c810147bdaca
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- **Renamed the gem from `rubocop-design` to `kaizo`**, and moved every cop from
|
|
6
|
+
the `Design/` department into `Kaizo/`: `Design/PositionalArguments` is now
|
|
7
|
+
`Kaizo/PositionalArguments`, `Design/SpecComment` is now `Kaizo/SpecComment`,
|
|
8
|
+
and so on for every cop. This is a pure rename — no cop logic, thresholds, or
|
|
9
|
+
messages changed. Update your `Gemfile` (`gem 'kaizo'`), your `.rubocop.yml`
|
|
10
|
+
`plugins:` entry (`- kaizo`), and any cop names in your configuration.
|
|
11
|
+
- Add `Kaizo/SpecDescriptionProse` cop: requires RSpec `it`/`context`
|
|
12
|
+
descriptions to read as one-behavior prose. An `it` description may not contain
|
|
13
|
+
a comma, a conjunction (`Conjunctions`), or code; a `context` description
|
|
14
|
+
carries no code and must open with a `ContextPrefixes` word. `describe` strings
|
|
15
|
+
are exempt. No autocorrection. Enabled by default.
|
|
16
|
+
- Add `Kaizo/FileUtilsInclusion` cop: requires `FileUtils` to be mixed in with
|
|
17
|
+
`include`/`extend` once it is used more than once in a class or module, instead
|
|
18
|
+
of repeating the `FileUtils.` receiver. A single qualified call, and a
|
|
19
|
+
namespace that already mixes it in, are left alone; nested classes/modules are
|
|
20
|
+
counted on their own. No autocorrection. Enabled by default.
|
|
21
|
+
- Add `Kaizo/PreferPathname` cop: flags calls to `File` class methods that have a
|
|
22
|
+
`Pathname` instance-method equivalent (`File.read`, `File.exist?`, `File.join`,
|
|
23
|
+
...), preferring `Pathname`. Runs on `**/*.rb`; `exe/**/*` and `bin/**/*` are
|
|
24
|
+
exempt by default (override with the standard `Include`/`Exclude`). No
|
|
25
|
+
autocorrection. Enabled by default.
|
|
26
|
+
- Add `Kaizo/ExplicitBegin` cop: requires an explicit `begin`/`end` block when
|
|
27
|
+
a method body attaches a `rescue` or `ensure` directly to the `def` (an
|
|
28
|
+
"implicit begin") -- the inverse of `Style/RedundantBegin`. Modifier `rescue`
|
|
29
|
+
(`foo rescue nil`) and endless method definitions are not flagged.
|
|
30
|
+
Autocorrection wraps the body in `begin`/`end`, and is skipped only for
|
|
31
|
+
single-line definitions and bodies holding heredocs or other multiline string
|
|
32
|
+
literals, where re-indenting could change string contents. Enabled by default.
|
|
33
|
+
- Add `Kaizo/NextInNonVoidEnumerable` cop: flags `next` inside the block of a
|
|
34
|
+
value-returning `Enumerable` method (`map`, `select`, `reduce`, and friends),
|
|
35
|
+
where `next` is being used as control-flow-as-value. Void iteration methods
|
|
36
|
+
(`each`, `each_with_object`, ...) and non-`Enumerable` loops (`loop`, `while`,
|
|
37
|
+
`Integer#times`) are never flagged, and a `next` bound to a nested block or
|
|
38
|
+
loop is attributed to that inner scope. Exempt methods with `AllowedMethods` /
|
|
39
|
+
`AllowedPatterns`. No autocorrection. Enabled by default.
|
|
40
|
+
- Loading the plugin now disables core's `Style/RedundantBegin`, which enforces
|
|
41
|
+
the opposite style; leaving both on would make their autocorrections loop.
|
|
42
|
+
Re-enable it explicitly in your `.rubocop.yml` if you do not want
|
|
43
|
+
`Kaizo/ExplicitBegin`.
|
|
44
|
+
|
|
45
|
+
## 0.5.0
|
|
46
|
+
|
|
47
|
+
- Add `Design/SpecComment` cop: flags comments in spec files (`**/*_spec.rb`),
|
|
48
|
+
on the principle that a comment in a spec usually wants to be a `context`/`it`
|
|
49
|
+
description or a better-structured example. Magic comments, `# rubocop:`
|
|
50
|
+
directives, and shebangs are exempt; add more with `AllowedPatterns`. No
|
|
51
|
+
autocorrection. Enabled by default.
|
|
52
|
+
|
|
53
|
+
## 0.4.0
|
|
54
|
+
|
|
55
|
+
- Add `Design/NestedMethodCalls` cop: flags method calls nested too deeply in
|
|
56
|
+
argument positions -- e.g. `foo(SomeClass.new(another("bar").chain))` -- on the
|
|
57
|
+
principle that the intermediate results want names. Only argument nesting is
|
|
58
|
+
counted (receiver chains are a separate concern); operator methods never count,
|
|
59
|
+
and `AllowedMethods` exempts named methods. Bounded by `Max` (default 1). No
|
|
60
|
+
autocorrection. Enabled by default.
|
|
61
|
+
|
|
62
|
+
## 0.3.0
|
|
63
|
+
|
|
64
|
+
- **Renamed the gem from `rubocop-arity` to `rubocop-design`**, and moved every
|
|
65
|
+
cop into a single `Design/` department: `Arity/PositionalArguments`,
|
|
66
|
+
`Arity/KeywordArguments`, and `Arity/TotalArguments` are now
|
|
67
|
+
`Design/PositionalArguments`, `Design/KeywordArguments`, and
|
|
68
|
+
`Design/TotalArguments`. Update your `Gemfile`, your `.rubocop.yml`
|
|
69
|
+
`plugins:` entry, and any cop names in your configuration.
|
|
70
|
+
|
|
71
|
+
## 0.2.0
|
|
72
|
+
|
|
73
|
+
- Add `Design/AgentNounClassName` cop: flags classes named as agent nouns
|
|
74
|
+
("doers") — names ending in `er`/`or`, or in a configured `ForbiddenSuffixes`
|
|
75
|
+
entry (default `Service`/`Util`/`Utils`) — unless they end in an
|
|
76
|
+
`AllowedSuffixes` entry. Checks `class` definitions and
|
|
77
|
+
`Struct.new`/`Data.define`/`Class.new` assignments. No autocorrection.
|
|
78
|
+
|
|
79
|
+
## 0.1.0
|
|
80
|
+
|
|
81
|
+
Initial release.
|
|
82
|
+
|
|
83
|
+
- Add `Arity/PositionalArguments` cop: bounds the number of positional
|
|
84
|
+
arguments (`Max`).
|
|
85
|
+
- Add `Arity/KeywordArguments` cop: bounds the number of keyword arguments
|
|
86
|
+
(`Max`).
|
|
87
|
+
- Add `Arity/TotalArguments` cop: bounds the combined number of positional and
|
|
88
|
+
keyword arguments (`Max`).
|
|
89
|
+
|
|
90
|
+
All three cops check `def`, `def self.`, `define_method`, and
|
|
91
|
+
`define_singleton_method`, ignore `*rest`/`**kwargs`/`&block`, and exempt the
|
|
92
|
+
`initialize` of a `Struct.new`/`Data.define` block.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flipmine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# Kaizo
|
|
2
|
+
|
|
3
|
+
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.
|
|
9
|
+
|
|
10
|
+
_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.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# bad
|
|
21
|
+
def calculate_volume(width, length, height, shape_type)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# good
|
|
25
|
+
def calculate_volume(shape)
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Cops
|
|
30
|
+
|
|
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)).
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
Add to your `Gemfile`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
gem 'kaizo', require: false
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Enable the plugin in `.rubocop.yml`:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
plugins:
|
|
79
|
+
- kaizo
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
(Requires RuboCop 1.72.2+ for the `lint_roller` plugin API.)
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
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:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
Kaizo/PositionalArguments:
|
|
92
|
+
Max: 1 # default
|
|
93
|
+
Kaizo/KeywordArguments:
|
|
94
|
+
Max: 1 # default
|
|
95
|
+
Kaizo/TotalArguments:
|
|
96
|
+
Max: 2 # default (one positional + one keyword)
|
|
97
|
+
```
|
|
98
|
+
|
|
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:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
Kaizo/PositionalArguments:
|
|
104
|
+
Max: 0 # every argument must be passed by keyword
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### What is counted
|
|
108
|
+
|
|
109
|
+
Only **named, individual** parameters count toward the limits:
|
|
110
|
+
|
|
111
|
+
- Positional: required (`a`) and optional (`a = 1`).
|
|
112
|
+
- Keyword: required (`a:`) and optional (`a: 1`).
|
|
113
|
+
|
|
114
|
+
The variadic collectors `*rest`, `**keyword_rest`, `&block`, and `...` are **not**
|
|
115
|
+
counted — they are single tokens, not a list of primitives.
|
|
116
|
+
|
|
117
|
+
### Exemptions
|
|
118
|
+
|
|
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:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# not flagged
|
|
125
|
+
Data.define(:width, :height, :depth, :weight) do
|
|
126
|
+
def initialize(width:, height:, depth:, weight:)
|
|
127
|
+
super
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
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.
|
|
134
|
+
|
|
135
|
+
## Relationship to `Metrics/ParameterLists`
|
|
136
|
+
|
|
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.
|
|
141
|
+
|
|
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).
|
|
145
|
+
|
|
146
|
+
## Known limitations
|
|
147
|
+
|
|
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.
|
|
158
|
+
|
|
159
|
+
## Class naming
|
|
160
|
+
|
|
161
|
+
`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.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# bad
|
|
169
|
+
class PaymentProcessor
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# good
|
|
173
|
+
class Payment
|
|
174
|
+
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
|
+
|
|
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.
|
|
194
|
+
|
|
195
|
+
Extend either list without restating the default using RuboCop's `inherit_mode`:
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
Kaizo/AgentNounClassName:
|
|
199
|
+
inherit_mode:
|
|
200
|
+
merge:
|
|
201
|
+
- AllowedSuffixes
|
|
202
|
+
- ForbiddenSuffixes
|
|
203
|
+
AllowedSuffixes:
|
|
204
|
+
- Ledger # OrderLedger now passes
|
|
205
|
+
ForbiddenSuffixes:
|
|
206
|
+
- Server # ApiServer now flagged, despite the default allowance
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Nested method calls
|
|
210
|
+
|
|
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.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# bad
|
|
219
|
+
wrap(parse(read(io)))
|
|
220
|
+
|
|
221
|
+
# good - name the intermediate result
|
|
222
|
+
parsed = parse(read(io))
|
|
223
|
+
wrap(parsed)
|
|
224
|
+
|
|
225
|
+
# good - a single nested call is fine
|
|
226
|
+
puts compute(value)
|
|
227
|
+
```
|
|
228
|
+
|
|
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:
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
Kaizo/NestedMethodCalls:
|
|
237
|
+
Max: 1 # default; raise to allow deeper nesting
|
|
238
|
+
AllowedMethods:
|
|
239
|
+
- expect # e.g. don't count RSpec's expect(...) wrapper
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Like the other cops, there is **no autocorrection** — choosing the intermediate
|
|
243
|
+
name is a design decision.
|
|
244
|
+
|
|
245
|
+
## Comments in specs
|
|
246
|
+
|
|
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.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# bad
|
|
255
|
+
it 'permits the request' do
|
|
256
|
+
# an admin can see everything
|
|
257
|
+
user = create(:user, admin: true)
|
|
258
|
+
expect(policy).to permit(user)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# good
|
|
262
|
+
it 'permits an admin to see everything' do
|
|
263
|
+
admin = create(:user, admin: true)
|
|
264
|
+
expect(policy).to permit(admin)
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
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):
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
Kaizo/SpecComment:
|
|
282
|
+
inherit_mode:
|
|
283
|
+
merge:
|
|
284
|
+
- Include
|
|
285
|
+
Include:
|
|
286
|
+
- '**/spec/**/*' # spec_helper, support/, factories
|
|
287
|
+
- '**/*_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:
|
|
295
|
+
AllowedPatterns:
|
|
296
|
+
- '\A#\s*@rbs' # rbs-inline type annotations
|
|
297
|
+
- 'noqa'
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Spec description prose
|
|
301
|
+
|
|
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.
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# 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
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# good
|
|
329
|
+
it "renders the name"
|
|
330
|
+
it "renders the cpu member"
|
|
331
|
+
context "when the role is unset" do
|
|
332
|
+
it "omits the key"
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
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.
|
|
344
|
+
|
|
345
|
+
## Including FileUtils
|
|
346
|
+
|
|
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.
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# bad
|
|
354
|
+
class Backup
|
|
355
|
+
def run
|
|
356
|
+
FileUtils.mkdir_p(dir)
|
|
357
|
+
FileUtils.cp(src, dir)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# good
|
|
362
|
+
class Backup
|
|
363
|
+
include FileUtils
|
|
364
|
+
|
|
365
|
+
def run
|
|
366
|
+
mkdir_p(dir)
|
|
367
|
+
cp(src, dir)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
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.
|
|
378
|
+
## Prefer Pathname
|
|
379
|
+
|
|
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
|
|
383
|
+
on it reads better than threading a string through `File`.
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
# bad
|
|
387
|
+
File.read(path)
|
|
388
|
+
File.exist?(path)
|
|
389
|
+
File.join(dir, name)
|
|
390
|
+
|
|
391
|
+
# good
|
|
392
|
+
path.read
|
|
393
|
+
path.exist?
|
|
394
|
+
dir.join(name)
|
|
395
|
+
```
|
|
396
|
+
|
|
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:
|
|
403
|
+
|
|
404
|
+
```yaml
|
|
405
|
+
Kaizo/PreferPathname:
|
|
406
|
+
Exclude:
|
|
407
|
+
- 'exe/**/*'
|
|
408
|
+
- 'bin/**/*'
|
|
409
|
+
- 'db/**/*' # add your own
|
|
410
|
+
```
|
|
411
|
+
|
|
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.
|
|
418
|
+
|
|
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.
|
|
422
|
+
|
|
423
|
+
## Explicit begin
|
|
424
|
+
|
|
425
|
+
`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.
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
# bad
|
|
434
|
+
def foo
|
|
435
|
+
do_something
|
|
436
|
+
ensure
|
|
437
|
+
cleanup
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# good
|
|
441
|
+
def foo
|
|
442
|
+
begin
|
|
443
|
+
do_something
|
|
444
|
+
ensure
|
|
445
|
+
cleanup
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
```
|
|
449
|
+
|
|
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.
|
|
457
|
+
|
|
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:
|
|
462
|
+
|
|
463
|
+
```yaml
|
|
464
|
+
Style/RedundantBegin:
|
|
465
|
+
Enabled: true # opt back out of Kaizo/ExplicitBegin
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Next in value-returning blocks
|
|
469
|
+
|
|
470
|
+
`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.
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# bad
|
|
477
|
+
array.map do |item|
|
|
478
|
+
next if skip?(item)
|
|
479
|
+
transform(item)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# bad - `next <value>` counts too
|
|
483
|
+
array.reduce(0) do |sum, item|
|
|
484
|
+
next sum if skip?(item)
|
|
485
|
+
sum + item
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# good - void iteration method; `next` just skips the iteration
|
|
489
|
+
array.each do |item|
|
|
490
|
+
next if skip?(item)
|
|
491
|
+
process(item)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# good - say what you mean
|
|
495
|
+
array.filter_map { |item| transform(item) unless skip?(item) }
|
|
496
|
+
```
|
|
497
|
+
|
|
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`:
|
|
510
|
+
|
|
511
|
+
```yaml
|
|
512
|
+
Kaizo/NextInNonVoidEnumerable:
|
|
513
|
+
AllowedMethods:
|
|
514
|
+
- reduce # allow `next <acc>` guards in reduce/inject
|
|
515
|
+
AllowedPatterns: []
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## Development
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
bin/setup # install dependencies
|
|
522
|
+
bundle exec rake # run specs + self-lint
|
|
523
|
+
bundle exec rake spec # specs only
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
Available as open source under the terms of the
|
|
529
|
+
[MIT License](https://opensource.org/licenses/MIT).
|