rubocop-kata 0.7.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +62 -0
- data/README.md +83 -2
- data/config/default.yml +110 -6
- data/exe/rubocop-kata +7 -2
- data/lib/rubocop/cop/kata/agent_noun.rb +6 -2
- data/lib/rubocop/cop/kata/env_discipline.rb +5 -0
- data/lib/rubocop/kata/kit.rb +23 -0
- data/lib/rubocop/kata/plan.rb +100 -0
- data/lib/rubocop/kata/version.rb +1 -1
- data/skills/rubocop-kata/SKILL.md +126 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da7876f7f43029f7dfb90b0fb9527987fe9c50ee40336d0ba4fffdcc2cbad83f
|
|
4
|
+
data.tar.gz: 902e0c01d3cc5e71553d3c90c68a707b7f3a554cc3e6ec1228f0640ae8772d6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99fdeb3a9f112fbc7232f59da504bde048071f20d50e55ef367e5516119a307a68a4f071a371cdbbe7810248268ef526f5bbdec08c5e9068e14d17cee03661ca
|
|
7
|
+
data.tar.gz: 9603d8560c96c877a5cb8f614bf10cab2aabf63aa5aace3209cc0596b06dab7a0edd4372919068a5c345b48cff076c1964b453ba38caa1dd3d026a96ad6c8371
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
- New `rubocop-kata plan` command: buckets the backlog into `structure`,
|
|
6
|
+
`naming`, `prose` and `rest`, and names the stage to take next. Kata's
|
|
7
|
+
structural cops create names and its naming cops charge for them, so the net
|
|
8
|
+
offense count moves the wrong way mid-refactor and cannot be read as progress.
|
|
9
|
+
- New `rubocop-kata skill` command: prints an agent skill to stdout, so
|
|
10
|
+
redirecting it installs the skill wherever an agent reads them from. It works
|
|
11
|
+
the stage `plan` names one file at a time, checks every name it introduces
|
|
12
|
+
against the dictionary `Kata/RealWords` reads, and reports removed and
|
|
13
|
+
created separately.
|
|
14
|
+
- `Kata/AgentNoun` no longer truncates a compound to its prefix. Truncation is
|
|
15
|
+
not injective — `EvidencePoller`, `EvidenceValidator` and `EvidenceWaiter`
|
|
16
|
+
were each told to become `Evidence` — and it lands on names that already
|
|
17
|
+
exist elsewhere, which a per-file cop cannot see. It now derives the agent
|
|
18
|
+
word and keeps the rest, so `EvidenceValidator` suggests
|
|
19
|
+
`EvidenceValidation`, and stays quiet when no derivation applies.
|
|
20
|
+
- `Kata/AgentNoun` `AllowedNames` gains `Controller`, `Mailer` and
|
|
21
|
+
`Serializer`: suffixes a framework resolves a class by are contracts, not
|
|
22
|
+
naming choices.
|
|
23
|
+
- `Kata/EnvDiscipline` no longer flags `ENV` as a parameter default.
|
|
24
|
+
`def enabled?(env = ENV)` is the injection the cop asks for: the parameter
|
|
25
|
+
is the seam, and the default is what the boot layer would pass anyway.
|
|
26
|
+
- Every cop that excluded `spec/**/*` now excludes `test/**/*` too. Fifteen
|
|
27
|
+
cops shipped an RSpec-only exclusion, so a Minitest suite got the
|
|
28
|
+
production rules.
|
|
29
|
+
- The boot layer that `Kata/EnvDiscipline`, `Kata/IoDiscipline` and
|
|
30
|
+
`Kata/ClockDiscipline` exempt now covers an application's, not only a gem's:
|
|
31
|
+
`config/`, `db/seeds.rb`, `db/seeds/`, `lib/tasks/`, `Rakefile` and
|
|
32
|
+
`*.rake`, alongside `bin/` and `exe/`.
|
|
33
|
+
- `Kata/NoComments` now exempts `Gemfile` as it already exempts `*.gemspec`. A
|
|
34
|
+
dependency manifest cannot say why a dependency is pinned in code.
|
|
35
|
+
- `Metrics/MethodLength` counts an array, hash, heredoc or method call that
|
|
36
|
+
spans lines as one line. `Max: 5` and `Layout/LineLength: 120` were
|
|
37
|
+
otherwise unsatisfiable together: wrapping a long line spent method budget.
|
|
38
|
+
- `Layout/ArgumentAlignment` now uses `with_fixed_indentation`. Its default
|
|
39
|
+
aligns continuation arguments under the first argument's column, a step
|
|
40
|
+
`Elegant/MonotonicIndents` rejects.
|
|
41
|
+
- `Layout/EmptyLinesAroundAttributeAccessor` is off. It requires a blank line
|
|
42
|
+
after `attr_reader` where `Elegant/NoEmptyLinesInBlocks` forbids one; inside
|
|
43
|
+
an `included do` block the two autocorrect in a loop.
|
|
44
|
+
- `Lint/NumberConversion` is off. Its message names a replacement that
|
|
45
|
+
raises: `Integer(x, 10)` is only valid when `x` is a String, and a `.to_i`
|
|
46
|
+
receiver is usually a Time, a BigDecimal or an Integer.
|
|
47
|
+
- The five bundled cops that exempt test files now also match Rails'
|
|
48
|
+
`foo_test.rb`, `foo_spec.rb`, and suites addressed by directory. Upstream
|
|
49
|
+
matched `**/*Test.rb` and `**/test_*.rb` only.
|
|
50
|
+
- `Elegant/ClassInModule` is off. Its offenses cannot be cleared: it reports a
|
|
51
|
+
class nested in a class as global, and `Elegant/NoClassInModule` forbids the
|
|
52
|
+
module its message asks for, so the pair admits no shape. It also reads a
|
|
53
|
+
top-level constant as a defect, which is how Rails resolves one. Reported
|
|
54
|
+
upstream as yegor256/rubocop-elegant#75.
|
|
55
|
+
- `Elegant/PairedBrackets` autocorrect is off. It inserts a newline beside a
|
|
56
|
+
bracket without taking the whitespace already there, so corrected lines drift
|
|
57
|
+
right and `Elegant/MonotonicIndents` then reports the line it just wrote. The
|
|
58
|
+
rule still reports; fix the brackets by hand. Reported upstream as
|
|
59
|
+
yegor256/rubocop-elegant#76.
|
|
60
|
+
- Development runs against giacope/rubocop-elegant#fixes, the released gem plus
|
|
61
|
+
five open pull requests. A gemspec cannot name a git source, so consumers who
|
|
62
|
+
want the fixes rather than the workarounds add the same line to their own
|
|
63
|
+
Gemfile; the README says how.
|
|
64
|
+
|
|
3
65
|
## [0.7.0] - 2026-08-16
|
|
4
66
|
|
|
5
67
|
- New `rubocop-kata doctor` command: reports `.rubocop.yml` entries that
|
data/README.md
CHANGED
|
@@ -53,13 +53,58 @@ bundle exec rubocop
|
|
|
53
53
|
|
|
54
54
|
That's it. The plugin loads the bundled extensions and the shared defaults, so your `.rubocop.yml` keeps only what's specific to your project. Requires Ruby >= 3.4 and RuboCop ~> 1.75.
|
|
55
55
|
|
|
56
|
+
### The bundled rubocop-elegant
|
|
57
|
+
|
|
58
|
+
The released `rubocop-elegant` 0.7.1 has four defects this project reported
|
|
59
|
+
upstream: `NoRedundantVariable` autocorrect corrupts Ruby 3.1 shorthand
|
|
60
|
+
([#74](https://github.com/yegor256/rubocop-elegant/issues/74)), `ClassInModule`
|
|
61
|
+
reports a class nested in a class as global
|
|
62
|
+
([#75](https://github.com/yegor256/rubocop-elegant/issues/75)),
|
|
63
|
+
`PairedBrackets` autocorrect drifts the indent
|
|
64
|
+
([#76](https://github.com/yegor256/rubocop-elegant/issues/76)), and the
|
|
65
|
+
test-file exclusions never match a Rails or RSpec suite
|
|
66
|
+
([#77](https://github.com/yegor256/rubocop-elegant/issues/77)).
|
|
67
|
+
|
|
68
|
+
The defaults here neutralise all four. `Elegant/ClassInModule` is off, because
|
|
69
|
+
its offences cannot be cleared: the cop it ships beside forbids the module it
|
|
70
|
+
asks for. The two broken correctors are set to report only, because their
|
|
71
|
+
offences can be cleared, by hand. The exclusions are widened. Nothing corrupts
|
|
72
|
+
your code out of the box, and nothing reports a defect you cannot fix.
|
|
73
|
+
|
|
74
|
+
A gemspec cannot name a git source, so to take the fixes themselves rather than
|
|
75
|
+
the workarounds, add this to your own `Gemfile`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
gem "rubocop-elegant", github: "giacope/rubocop-elegant", branch: "fixes"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That branch is the released gem plus the five pull requests, with the version
|
|
82
|
+
pinned so Bundler resolves it. With it in place you can turn the two correctors
|
|
83
|
+
back on:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
Elegant/NoRedundantVariable:
|
|
87
|
+
AutoCorrect: true
|
|
88
|
+
|
|
89
|
+
Elegant/PairedBrackets:
|
|
90
|
+
AutoCorrect: true
|
|
91
|
+
|
|
92
|
+
Elegant/ClassInModule:
|
|
93
|
+
Enabled: true
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Drop all three once upstream releases the fixes. On a Rails codebase, consider
|
|
97
|
+
leaving `Elegant/ClassInModule` off for good: it wants every class inside a
|
|
98
|
+
module, and Zeitwerk resolves `class Account` from `app/models/account.rb` as a
|
|
99
|
+
top-level constant by design.
|
|
100
|
+
|
|
56
101
|
## The cops
|
|
57
102
|
|
|
58
103
|
| Cop | Default | What it enforces |
|
|
59
104
|
| --- | --- | --- |
|
|
60
105
|
| `Kata/AgentNoun` | on | Classes named for what they are, not `-er`/`-or` doers. Suggests the better name when it can derive one. `AllowedNames` matches a whole name or a trailing segment, so `Error` covers `UsageError`. |
|
|
61
106
|
| `Kata/NoComments` | on | No prose comments; say it in the code. Magic comments, linter directives, and licence headers survive. Autocorrects. |
|
|
62
|
-
| `Kata/IoDiscipline` | on | No bare `puts`/`warn`/`pp`/`p` outside
|
|
107
|
+
| `Kata/IoDiscipline` | on | No bare `puts`/`warn`/`pp`/`p` outside the test suite and the boot layer; write through an injected `@io` or an explicit receiver. |
|
|
63
108
|
| `Kata/ProsePlacement` | off | Sentence-length strings belong in the presentation layer. Enable with an `Include`/`Exclude` matching your layering. |
|
|
64
109
|
| `Kata/NoUtilName` | on | No junk-drawer names (`Util`, `Helper`, `Manager`, `Service`, …). Tune via `BannedNames`. |
|
|
65
110
|
| `Kata/RealWords` | on | Every name segment is a word the shipped dictionary knows — `errorcount` (smash) and `cfg` (abbreviation) both fail, with no word list to maintain. Tune via `Terms`/`BannedWords`/`AllowedNames`. |
|
|
@@ -71,7 +116,7 @@ That's it. The plugin loads the bundled extensions and the shared defaults, so y
|
|
|
71
116
|
| `Kata/NoClassMethodLogic` | on | Class methods construct (`build`, `parse`, `of`, `from_*`); instances do the work. |
|
|
72
117
|
| `Kata/NoHashAsObject` | on | A hash with `MaxKeys`+ keys (default 4) wants to be an object. Keyword-argument call sites exempt. |
|
|
73
118
|
| `Kata/ClockDiscipline` | on | No bare `Time.now`/`Date.today`/`.current`; inject a clock. |
|
|
74
|
-
| `Kata/EnvDiscipline` | on | `ENV` reads only in the boot layer (`config/`, `bin/`, `exe/`). |
|
|
119
|
+
| `Kata/EnvDiscipline` | on | `ENV` reads only in the boot layer (`config/`, `db/seeds*`, `lib/tasks/`, rake files, `bin/`, `exe/`) — or as a parameter default, which is the seam the cop asks for. |
|
|
75
120
|
|
|
76
121
|
## Dead configuration
|
|
77
122
|
|
|
@@ -92,6 +137,42 @@ lib/registry.rb:44: the directive names `Elegant/GoodMethodName`, which is not e
|
|
|
92
137
|
|
|
93
138
|
It exits non-zero when it finds something, so it can gate CI.
|
|
94
139
|
|
|
140
|
+
## Adoption order
|
|
141
|
+
|
|
142
|
+
Kata's structural cops create names and its naming cops charge for them, so a
|
|
143
|
+
structural refactor removes offenses and adds more. A single total cannot tell
|
|
144
|
+
progress from regression. `plan` buckets the backlog into the stages that cause
|
|
145
|
+
each other and names the one to take next.
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
bundle exec rubocop-kata plan # or: plan path/to/project
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
structure 184 Kata/ConstructorDiscipline 92, Kata/NoHashAsObject 48, Kata/EnvDiscipline 44
|
|
153
|
+
naming 263 Kata/RealWords 141, Kata/GoodVariableName 88, Kata/AgentNoun 34
|
|
154
|
+
prose 57 Kata/NoComments 57
|
|
155
|
+
rest 412 Elegant/PairedBrackets 300, Layout/LineLength 112
|
|
156
|
+
next: structure — 184 offenses in 61 files; these mint the names `naming` then prices, so take them first
|
|
157
|
+
densest: app/models/account.rb (14)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Structure before naming, because doing naming first means renaming things the
|
|
161
|
+
structural pass is about to move.
|
|
162
|
+
|
|
163
|
+
The gem also ships an agent skill that runs this loop: it works the stage `plan`
|
|
164
|
+
names, checks every name it introduces against the same dictionary
|
|
165
|
+
`Kata/RealWords` reads, and reports removed and created separately instead of a
|
|
166
|
+
net total.
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
mkdir -p .claude/skills/rubocop-kata
|
|
170
|
+
bundle exec rubocop-kata skill > .claude/skills/rubocop-kata/SKILL.md
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It writes to stdout, so the same command installs it anywhere an agent reads
|
|
174
|
+
skills from — a project, `~/.claude/skills/`, or a plugin.
|
|
175
|
+
|
|
95
176
|
## The defaults
|
|
96
177
|
|
|
97
178
|
Double-quoted strings, `Metrics/MethodLength: 5`, `Metrics/ClassLength: 100`,
|
data/config/default.yml
CHANGED
|
@@ -14,20 +14,28 @@ Kata/AgentNoun:
|
|
|
14
14
|
Description: >-
|
|
15
15
|
Name classes for the thing they are, not the work they do. `AllowedNames`
|
|
16
16
|
entries match a whole name or a trailing segment of one, so `Error` also
|
|
17
|
-
exempts `UsageError`.
|
|
17
|
+
exempts `UsageError`. The default list holds the suffixes a framework
|
|
18
|
+
resolves a class by, which are contracts rather than naming choices.
|
|
18
19
|
Enabled: true
|
|
19
20
|
VersionAdded: "0.1"
|
|
20
|
-
VersionChanged: "0.
|
|
21
|
+
VersionChanged: "0.8"
|
|
21
22
|
AllowedNames:
|
|
22
23
|
- Error
|
|
24
|
+
- Controller
|
|
25
|
+
- Mailer
|
|
26
|
+
- Serializer
|
|
23
27
|
|
|
24
28
|
Kata/NoComments:
|
|
25
29
|
Description: "Say it in the code; keep only comments the machine or the law reads."
|
|
26
30
|
Enabled: true
|
|
27
31
|
VersionAdded: "0.1"
|
|
32
|
+
VersionChanged: "0.8"
|
|
28
33
|
Exclude:
|
|
29
34
|
- "spec/**/*"
|
|
35
|
+
- "test/**/*"
|
|
30
36
|
- "*.gemspec"
|
|
37
|
+
- "Gemfile"
|
|
38
|
+
- "**/Gemfile"
|
|
31
39
|
|
|
32
40
|
Kata/GoodMethodName:
|
|
33
41
|
Description: >-
|
|
@@ -145,11 +153,18 @@ Kata/IoDiscipline:
|
|
|
145
153
|
Description: "Write through an injected io, not bare puts/warn/pp/p."
|
|
146
154
|
Enabled: true
|
|
147
155
|
VersionAdded: "0.1"
|
|
156
|
+
VersionChanged: "0.8"
|
|
148
157
|
Exclude:
|
|
149
158
|
- "spec/**/*"
|
|
159
|
+
- "test/**/*"
|
|
150
160
|
- "bin/**/*"
|
|
151
161
|
- "exe/**/*"
|
|
162
|
+
- "config/**/*"
|
|
163
|
+
- "db/seeds.rb"
|
|
164
|
+
- "db/seeds/**/*"
|
|
165
|
+
- "lib/tasks/**/*"
|
|
152
166
|
- "Rakefile"
|
|
167
|
+
- "**/*.rake"
|
|
153
168
|
- "*.gemspec"
|
|
154
169
|
|
|
155
170
|
Kata/ProsePlacement:
|
|
@@ -221,6 +236,7 @@ Kata/NoBooleanFlag:
|
|
|
221
236
|
VersionAdded: "0.3"
|
|
222
237
|
Exclude:
|
|
223
238
|
- "spec/**/*"
|
|
239
|
+
- "test/**/*"
|
|
224
240
|
|
|
225
241
|
Kata/ConstructorDiscipline:
|
|
226
242
|
Description: "Constructors assign; they do not compute."
|
|
@@ -228,6 +244,7 @@ Kata/ConstructorDiscipline:
|
|
|
228
244
|
VersionAdded: "0.3"
|
|
229
245
|
Exclude:
|
|
230
246
|
- "spec/**/*"
|
|
247
|
+
- "test/**/*"
|
|
231
248
|
|
|
232
249
|
Kata/NoClassMethodLogic:
|
|
233
250
|
Description: "Classes construct; instances work."
|
|
@@ -235,6 +252,7 @@ Kata/NoClassMethodLogic:
|
|
|
235
252
|
VersionAdded: "0.3"
|
|
236
253
|
Exclude:
|
|
237
254
|
- "spec/**/*"
|
|
255
|
+
- "test/**/*"
|
|
238
256
|
AllowedNames:
|
|
239
257
|
- build
|
|
240
258
|
- parse
|
|
@@ -247,27 +265,43 @@ Kata/NoHashAsObject:
|
|
|
247
265
|
MaxKeys: 4
|
|
248
266
|
Exclude:
|
|
249
267
|
- "spec/**/*"
|
|
268
|
+
- "test/**/*"
|
|
250
269
|
- "config/**/*"
|
|
251
270
|
|
|
252
271
|
Kata/ClockDiscipline:
|
|
253
272
|
Description: "Read time from an injected clock, not the ambient one."
|
|
254
273
|
Enabled: true
|
|
255
274
|
VersionAdded: "0.3"
|
|
275
|
+
VersionChanged: "0.8"
|
|
256
276
|
Exclude:
|
|
257
277
|
- "spec/**/*"
|
|
278
|
+
- "test/**/*"
|
|
258
279
|
- "bin/**/*"
|
|
259
280
|
- "exe/**/*"
|
|
281
|
+
- "config/**/*"
|
|
282
|
+
- "db/seeds.rb"
|
|
283
|
+
- "db/seeds/**/*"
|
|
284
|
+
- "lib/tasks/**/*"
|
|
285
|
+
- "Rakefile"
|
|
286
|
+
- "**/*.rake"
|
|
287
|
+
- "*.gemspec"
|
|
260
288
|
|
|
261
289
|
Kata/EnvDiscipline:
|
|
262
290
|
Description: "ENV belongs in the boot layer; pass configuration in."
|
|
263
291
|
Enabled: true
|
|
264
292
|
VersionAdded: "0.3"
|
|
293
|
+
VersionChanged: "0.8"
|
|
265
294
|
Exclude:
|
|
266
295
|
- "spec/**/*"
|
|
267
|
-
- "
|
|
296
|
+
- "test/**/*"
|
|
268
297
|
- "bin/**/*"
|
|
269
298
|
- "exe/**/*"
|
|
299
|
+
- "config/**/*"
|
|
300
|
+
- "db/seeds.rb"
|
|
301
|
+
- "db/seeds/**/*"
|
|
302
|
+
- "lib/tasks/**/*"
|
|
270
303
|
- "Rakefile"
|
|
304
|
+
- "**/*.rake"
|
|
271
305
|
- "*.gemspec"
|
|
272
306
|
|
|
273
307
|
Style/Documentation:
|
|
@@ -314,26 +348,42 @@ Layout/LineEndStringConcatenationIndentation:
|
|
|
314
348
|
Metrics/BlockLength:
|
|
315
349
|
Exclude:
|
|
316
350
|
- "spec/**/*"
|
|
351
|
+
- "test/**/*"
|
|
317
352
|
- "*.gemspec"
|
|
318
353
|
|
|
319
354
|
Metrics/ClassLength:
|
|
320
355
|
Max: 100
|
|
356
|
+
Exclude:
|
|
357
|
+
- "spec/**/*"
|
|
358
|
+
- "test/**/*"
|
|
359
|
+
- "**/*_test.rb"
|
|
321
360
|
|
|
322
361
|
Metrics/ModuleLength:
|
|
323
362
|
Max: 100
|
|
324
363
|
Exclude:
|
|
325
364
|
- "spec/**/*"
|
|
365
|
+
- "test/**/*"
|
|
366
|
+
- "**/*_test.rb"
|
|
326
367
|
|
|
368
|
+
# A wrapped call reads as one line, so it costs one; otherwise Max and
|
|
369
|
+
# Layout/LineLength cannot both be satisfied.
|
|
327
370
|
Metrics/MethodLength:
|
|
328
371
|
Max: 5
|
|
372
|
+
CountAsOne:
|
|
373
|
+
- array
|
|
374
|
+
- hash
|
|
375
|
+
- heredoc
|
|
376
|
+
- method_call
|
|
329
377
|
Exclude:
|
|
330
378
|
- "spec/**/*"
|
|
379
|
+
- "test/**/*"
|
|
331
380
|
|
|
332
381
|
Metrics/ParameterLists:
|
|
333
382
|
Max: 4
|
|
334
383
|
CountKeywordArgs: true
|
|
335
384
|
Exclude:
|
|
336
385
|
- "spec/**/*"
|
|
386
|
+
- "test/**/*"
|
|
337
387
|
|
|
338
388
|
RSpec/ExampleLength:
|
|
339
389
|
CountAsOne:
|
|
@@ -345,10 +395,12 @@ RSpec/VerifiedDoubles:
|
|
|
345
395
|
Elegant/NoEmptyLinesInBlocks:
|
|
346
396
|
Exclude:
|
|
347
397
|
- "spec/**/*"
|
|
398
|
+
- "test/**/*"
|
|
348
399
|
|
|
349
400
|
Elegant/NoEmptyLinesInMethods:
|
|
350
401
|
Exclude:
|
|
351
402
|
- "spec/**/*"
|
|
403
|
+
- "test/**/*"
|
|
352
404
|
|
|
353
405
|
Elegant/NoComments:
|
|
354
406
|
Enabled: false
|
|
@@ -370,6 +422,16 @@ Layout/ClosingParenthesisIndentation:
|
|
|
370
422
|
Layout/FirstArgumentIndentation:
|
|
371
423
|
Enabled: false
|
|
372
424
|
|
|
425
|
+
# Its default aligns continuation arguments under the first argument's column,
|
|
426
|
+
# an arbitrary step Elegant/MonotonicIndents rejects; a fixed step is +2.
|
|
427
|
+
Layout/ArgumentAlignment:
|
|
428
|
+
EnforcedStyle: with_fixed_indentation
|
|
429
|
+
|
|
430
|
+
# Requires a blank line after `attr_reader` where Elegant/NoEmptyLinesInBlocks
|
|
431
|
+
# forbids one; inside an `included do` block the two loop.
|
|
432
|
+
Layout/EmptyLinesAroundAttributeAccessor:
|
|
433
|
+
Enabled: false
|
|
434
|
+
|
|
373
435
|
Layout/MultilineMethodCallBraceLayout:
|
|
374
436
|
Enabled: false
|
|
375
437
|
|
|
@@ -396,15 +458,21 @@ Style/DisableCopsWithinSourceCodeDirective:
|
|
|
396
458
|
Elegant/NoRedundantVariable:
|
|
397
459
|
AutoCorrect: false
|
|
398
460
|
|
|
461
|
+
# Inserts a newline beside a bracket without taking the whitespace already
|
|
462
|
+
# there, so every corrected line drifts right and MonotonicIndents then
|
|
463
|
+
# reports the line it just wrote.
|
|
464
|
+
Elegant/PairedBrackets:
|
|
465
|
+
AutoCorrect: false
|
|
466
|
+
|
|
399
467
|
# Rewrites reopened classes into modules, clashing with the primary
|
|
400
468
|
# class definition at boot.
|
|
401
469
|
Style/StaticClass:
|
|
402
470
|
AutoCorrect: false
|
|
403
471
|
|
|
404
|
-
#
|
|
405
|
-
#
|
|
472
|
+
# Its message names a replacement that raises: Integer(x, 10) is only
|
|
473
|
+
# valid when x is a String, and most `.to_i` receivers are not.
|
|
406
474
|
Lint/NumberConversion:
|
|
407
|
-
|
|
475
|
+
Enabled: false
|
|
408
476
|
|
|
409
477
|
# Alphabetizing requires breaks load-order-dependent boot sequences.
|
|
410
478
|
Style/RequireOrder:
|
|
@@ -413,3 +481,39 @@ Style/RequireOrder:
|
|
|
413
481
|
ThreadSafety/DirChdir:
|
|
414
482
|
Exclude:
|
|
415
483
|
- "spec/**/*"
|
|
484
|
+
- "test/**/*"
|
|
485
|
+
|
|
486
|
+
# --- Elegant's test-file exclusions never match Rails' `foo_test.rb`, nor a
|
|
487
|
+
# suite addressed by directory.
|
|
488
|
+
# Off until upstream ships: it reports a class nested in a class as global, and
|
|
489
|
+
# Elegant/NoClassInModule forbids the module the message asks for, so the pair
|
|
490
|
+
# admits no shape. It also reads a top-level constant as a defect, which is how
|
|
491
|
+
# Rails resolves one. https://github.com/yegor256/rubocop-elegant/issues/75
|
|
492
|
+
Elegant/ClassInModule:
|
|
493
|
+
Enabled: false
|
|
494
|
+
inherit_mode:
|
|
495
|
+
merge:
|
|
496
|
+
- Exclude
|
|
497
|
+
Exclude:
|
|
498
|
+
- "**/*_test.rb"
|
|
499
|
+
- "test/**/*"
|
|
500
|
+
- "spec/**/*"
|
|
501
|
+
|
|
502
|
+
Elegant/NoClassInModule:
|
|
503
|
+
inherit_mode:
|
|
504
|
+
merge:
|
|
505
|
+
- Exclude
|
|
506
|
+
Exclude:
|
|
507
|
+
- "**/*_test.rb"
|
|
508
|
+
- "test/**/*"
|
|
509
|
+
- "spec/**/*"
|
|
510
|
+
|
|
511
|
+
Elegant/OneClassPerFile:
|
|
512
|
+
inherit_mode:
|
|
513
|
+
merge:
|
|
514
|
+
- Exclude
|
|
515
|
+
Exclude:
|
|
516
|
+
- "**/*_test.rb"
|
|
517
|
+
- "test/**/*"
|
|
518
|
+
- "spec/**/*"
|
|
519
|
+
|
data/exe/rubocop-kata
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
require "rubocop-kata"
|
|
5
5
|
require "rubocop/kata/checkup"
|
|
6
|
+
require "rubocop/kata/plan"
|
|
7
|
+
require "rubocop/kata/kit"
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
COMMANDS = { "doctor" => RuboCop::Kata::Checkup, "plan" => RuboCop::Kata::Plan, "skill" => RuboCop::Kata::Kit }.freeze
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
command = COMMANDS[ARGV.first]
|
|
12
|
+
abort("usage: rubocop-kata #{COMMANDS.keys.join("|")} [path]") if command.nil?
|
|
13
|
+
|
|
14
|
+
exit(command.new(ARGV.fetch(1, Dir.pwd)).run)
|
|
@@ -26,9 +26,13 @@ class RuboCop::Cop::Kata::AgentNoun < RuboCop::Cop::Base
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def suggestion(name)
|
|
29
|
+
head, tail = split(name)
|
|
30
|
+
DERIVATIONS.filter_map { proposal(head + tail.sub(it.first, it.last)) }.first
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def split(name)
|
|
29
34
|
segments = name.scan(SEGMENT)
|
|
30
|
-
|
|
31
|
-
DERIVATIONS.filter_map { proposal(name.sub(it.first, it.last)) }.first
|
|
35
|
+
[segments[0..-2].join, segments.last.to_s]
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def proposal(name)
|
|
@@ -6,6 +6,11 @@ class RuboCop::Cop::Kata::EnvDiscipline < RuboCop::Cop::Base
|
|
|
6
6
|
def on_const(node)
|
|
7
7
|
return unless node.short_name == :ENV
|
|
8
8
|
return unless node.namespace.nil? || node.namespace.cbase_type?
|
|
9
|
+
return if seam?(node)
|
|
9
10
|
add_offense(node, message: MSG)
|
|
10
11
|
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def seam?(node) = node.each_ancestor(:optarg, :kwoptarg).any?
|
|
11
16
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RuboCop::Kata::Kit
|
|
4
|
+
SOURCE = File.expand_path("../../../skills/rubocop-kata/SKILL.md", __dir__)
|
|
5
|
+
MISSING = "the gem ships no skill at %s"
|
|
6
|
+
|
|
7
|
+
def initialize(_root, io: $stdout)
|
|
8
|
+
@io = io
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
return missing unless File.file?(SOURCE)
|
|
13
|
+
@io.print(File.read(SOURCE))
|
|
14
|
+
0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def missing
|
|
20
|
+
@io.puts(format(MISSING, SOURCE))
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
class RuboCop::Kata::Plan
|
|
7
|
+
STAGES = {
|
|
8
|
+
"structure" => %w[
|
|
9
|
+
Kata/ConstructorDiscipline Kata/NoClassMethodLogic Kata/NoHashAsObject
|
|
10
|
+
Kata/NoBooleanFlag Kata/IoDiscipline Kata/ClockDiscipline Kata/EnvDiscipline
|
|
11
|
+
],
|
|
12
|
+
"naming" => %w[
|
|
13
|
+
Kata/RealWords Kata/GoodMethodName Kata/GoodVariableName Kata/AgentNoun
|
|
14
|
+
Kata/BuilderNoun Kata/NoUtilName
|
|
15
|
+
],
|
|
16
|
+
"prose" => %w[Kata/NoComments Kata/ProsePlacement]
|
|
17
|
+
}.freeze
|
|
18
|
+
REST = "rest"
|
|
19
|
+
ROW = "%-10s %6d %s"
|
|
20
|
+
NEXT = "next: %s — %d offense%s in %d file%s%s"
|
|
21
|
+
MINTS = "; these mint the names `naming` then prices, so take them first"
|
|
22
|
+
DENSE = "densest: %s (%d)"
|
|
23
|
+
CLEAN = "clean"
|
|
24
|
+
|
|
25
|
+
def initialize(root, io: $stdout)
|
|
26
|
+
@root = root
|
|
27
|
+
@io = io
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
found = offenses
|
|
32
|
+
return clean if found.empty?
|
|
33
|
+
STAGES.keys.push(REST).each { row(it, found) }
|
|
34
|
+
advise(found)
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def clean
|
|
41
|
+
@io.puts(CLEAN)
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def row(stage, found)
|
|
46
|
+
picked = within(found, stage)
|
|
47
|
+
return if picked.empty?
|
|
48
|
+
@io.puts(format(ROW, stage, picked.length, breakdown(picked)))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def breakdown(picked)
|
|
52
|
+
picked.group_by { it.fetch("cop") }
|
|
53
|
+
.sort_by { |_, list| -list.length }
|
|
54
|
+
.first(3)
|
|
55
|
+
.map { |cop, list| "#{cop} #{list.length}" }
|
|
56
|
+
.join(", ")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def advise(found)
|
|
60
|
+
picked = within(found, upcoming(found))
|
|
61
|
+
@io.puts(headline(upcoming(found), picked))
|
|
62
|
+
@io.puts(format(DENSE, *hottest(picked)))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def upcoming(found) = STAGES.keys.find { |stage| within(found, stage).any? } || REST
|
|
66
|
+
|
|
67
|
+
def headline(stage, picked)
|
|
68
|
+
files = picked.map { it.fetch("path") }.uniq
|
|
69
|
+
format(NEXT, stage, picked.length, plural(picked), files.length, plural(files), note(stage))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def note(stage) = stage == REST ? "" : MINTS
|
|
73
|
+
|
|
74
|
+
def hottest(picked) = picked.map { it.fetch("path") }.tally.max_by(&:last)
|
|
75
|
+
|
|
76
|
+
def plural(list) = list.one? ? "" : "s"
|
|
77
|
+
|
|
78
|
+
def within(found, stage) = found.select { it.fetch("stage") == stage }
|
|
79
|
+
|
|
80
|
+
def offenses = report.fetch("files", []).flat_map { rows(it) }
|
|
81
|
+
|
|
82
|
+
def rows(file)
|
|
83
|
+
file.fetch("offenses", []).map do
|
|
84
|
+
{ "path" => file.fetch("path"), "cop" => it.fetch("cop_name"), "stage" => stage(it.fetch("cop_name")) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stage(cop) = STAGES.find { |_, cops| cops.include?(cop) }&.first || REST
|
|
89
|
+
|
|
90
|
+
def report = @_report ||= JSON.parse(inspection)
|
|
91
|
+
|
|
92
|
+
def inspection = Dir.chdir(@root) { sweep }
|
|
93
|
+
|
|
94
|
+
def sweep
|
|
95
|
+
Tempfile.create("rubocop-kata-plan") do |sink|
|
|
96
|
+
RuboCop::CLI.new.run(["--format", "json", "--out", sink.path, "."])
|
|
97
|
+
File.read(sink.path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/rubocop/kata/version.rb
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rubocop-kata
|
|
3
|
+
description: >-
|
|
4
|
+
Adopt rubocop-kata's rules on an existing codebase without the offense count going
|
|
5
|
+
the wrong way. Kata's structural cops (ConstructorDiscipline, NoHashAsObject,
|
|
6
|
+
Io/Clock/EnvDiscipline, NoClassMethodLogic, NoBooleanFlag) create names, and its naming
|
|
7
|
+
cops (RealWords, GoodMethodName, GoodVariableName, AgentNoun, BuilderNoun, NoUtilName)
|
|
8
|
+
charge for every name created — so a structural refactor removes offenses and adds more,
|
|
9
|
+
and a single total cannot tell progress from regression. This skill sequences the work by
|
|
10
|
+
stage, picks names that are already clean by checking them against the dictionary the cops
|
|
11
|
+
read, and reports removed and created separately. Use when adopting rubocop-kata, working
|
|
12
|
+
down a large kata backlog, deciding what to fix next, or when kata offenses went up after a
|
|
13
|
+
refactor. Triggers on "rubocop-kata", "kata offenses", "kata offences", "kata backlog",
|
|
14
|
+
"adopt kata".
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# rubocop-kata adoption
|
|
18
|
+
|
|
19
|
+
> The count goes up mid-refactor because the refactor is doing its job. Stop reading the
|
|
20
|
+
> total; read the two numbers underneath it.
|
|
21
|
+
|
|
22
|
+
## The problem this solves
|
|
23
|
+
|
|
24
|
+
Move a collaborator out of a constructor and you satisfy `Kata/ConstructorDiscipline`. You
|
|
25
|
+
also mint a parameter, an ivar, and probably a reader — three names, each priced by
|
|
26
|
+
`Kata/RealWords`, `Kata/GoodVariableName`, and `Kata/BuilderNoun`. Net offenses can rise
|
|
27
|
+
while the code strictly improves. There is no configuration that fixes this, because the
|
|
28
|
+
names are real and someone has to choose them.
|
|
29
|
+
|
|
30
|
+
So choose them well the first time. Every name is checkable *before* it is written, against
|
|
31
|
+
the same dictionary the cops read. Do that and the naming debt is never created.
|
|
32
|
+
|
|
33
|
+
## Procedure
|
|
34
|
+
|
|
35
|
+
### 1. Clear dead configuration first
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
rubocop-kata doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It reports `.rubocop.yml` entries for cops the inherited config already disables, and
|
|
42
|
+
`rubocop:disable` comments naming a cop that is not enabled where the comment sits. RuboCop
|
|
43
|
+
reports neither. Delete what it names — it is free, and it stops you tuning knobs that are
|
|
44
|
+
not connected to anything.
|
|
45
|
+
|
|
46
|
+
### 2. Read the plan
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
rubocop-kata plan
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It buckets every offense into four stages and names the one to work:
|
|
53
|
+
|
|
54
|
+
- **structure** — the cops that change the shape of the code, and in doing so mint names.
|
|
55
|
+
- **naming** — the cops that price names.
|
|
56
|
+
- **prose** — `NoComments`, `ProsePlacement`.
|
|
57
|
+
- **rest** — bundled and core cops; mostly mechanical, mostly autocorrectable.
|
|
58
|
+
|
|
59
|
+
Work the stage `plan` names, in that order. Structure before naming is the whole point: doing
|
|
60
|
+
naming first means renaming things the structural pass is about to move or delete.
|
|
61
|
+
|
|
62
|
+
### 3. Work one file at a time
|
|
63
|
+
|
|
64
|
+
Take the file `plan` calls densest. Fix its offenses for the **current stage only** — leave
|
|
65
|
+
the other stages' offenses alone even when they are on the line you are editing. Mixing
|
|
66
|
+
stages is what makes a diff unreviewable and a count unreadable.
|
|
67
|
+
|
|
68
|
+
For `rest`, prefer `rubocop -a` (safe corrections). Do not run `-A` (unsafe) across a
|
|
69
|
+
codebase; kata turns off the correctors known to rewrite code wrongly, but unsafe correctors
|
|
70
|
+
outside that list still change behaviour.
|
|
71
|
+
|
|
72
|
+
### 4. Check every name before you write it
|
|
73
|
+
|
|
74
|
+
This is the step that keeps the count honest. Kata's naming rules, condensed:
|
|
75
|
+
|
|
76
|
+
| rule | cop |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| one word, lowercase, ≤16 chars | `GoodMethodName`, `GoodVariableName` |
|
|
79
|
+
| a second word only via a role prefix (`after_fork`), a role suffix (`file_of`), or a reviewed `Terms` entry | same |
|
|
80
|
+
| never smash the underscore out — `errorcount` fails too | `RealWords` |
|
|
81
|
+
| every segment is a real word; no `cfg`, `ctx`, `msg`, `tmp`, … | `RealWords` |
|
|
82
|
+
| classes are not `-er`/`-or` doers | `AgentNoun` |
|
|
83
|
+
| no `Util`, `Helper`, `Manager`, `Service`, `Common`, `Shared` | `NoUtilName` |
|
|
84
|
+
| a method that returns something is named for what it returns, not `get_`/`calculate_`/`compute_` | `BuilderNoun` |
|
|
85
|
+
| memoization ivars are `@_name` | `Naming/MemoizedInstanceVariableName` |
|
|
86
|
+
|
|
87
|
+
Check a candidate against the shipped dictionary before committing to it:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
ruby -rrubocop-kata -e 'puts ARGV.map { |w| "#{w}: #{RuboCop::Kata::Dictionary::ENTRIES.include?(w.downcase)}" }' clock sink cursor
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If a word the domain genuinely needs is not in the dictionary, that is a `Terms` entry in
|
|
94
|
+
`.rubocop.yml`, not a reason to pick a worse name — but add it deliberately, one at a time,
|
|
95
|
+
and say why in the commit.
|
|
96
|
+
|
|
97
|
+
When a name is genuinely two words and neither the prefix nor the suffix list fits, that is
|
|
98
|
+
usually the cop telling you an object is missing. `evidence_poller` wants to be
|
|
99
|
+
`Evidence#poll` or an `EvidenceValidation`. Prefer extracting the object over adding a
|
|
100
|
+
`Terms` entry.
|
|
101
|
+
|
|
102
|
+
### 5. Re-run `plan` and report two numbers
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
rubocop-kata plan
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Report the delta as **removed** and **created**, never as a net total:
|
|
109
|
+
|
|
110
|
+
> structure 184 → 142 (−42). naming 263 → 263 (+0 created, 21 names introduced, all clean).
|
|
111
|
+
|
|
112
|
+
A rising `naming` number after a structural pass means step 4 was skipped, not that the
|
|
113
|
+
refactor was wrong. Go back and fix the names you just wrote — it is cheaper now than after
|
|
114
|
+
they spread.
|
|
115
|
+
|
|
116
|
+
## Rules of engagement
|
|
117
|
+
|
|
118
|
+
- **Never `rubocop --auto-gen-config` to make a stage disappear.** A todo file converts a
|
|
119
|
+
backlog you can see into one you cannot. If a cop is genuinely wrong for this codebase,
|
|
120
|
+
turn it off in `.rubocop.yml` with a comment saying why; `doctor` will tell you later if
|
|
121
|
+
that entry stops meaning anything.
|
|
122
|
+
- **Never widen `AllowedNames`/`Terms` in bulk.** One entry, one reviewed reason.
|
|
123
|
+
- **One stage per commit.** The reviewer needs to see a structural move as a structural move.
|
|
124
|
+
- **Stop and ask** when a fix requires a decision you cannot verify from the code: what a
|
|
125
|
+
domain word means, whether a class is part of a public API, whether a framework resolves a
|
|
126
|
+
class by its name. Guessing on those creates rework that lints clean.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-kata
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Giacomo GK
|
|
@@ -112,7 +112,9 @@ description: Bundles a curated RuboCop stack (rspec, performance, elegant, packa
|
|
|
112
112
|
and adds fifteen cops of its own — naming (GoodMethodName, GoodVariableName, dictionary-backed
|
|
113
113
|
RealWords, AgentNoun, BuilderNoun, NoUtilName), discipline (IoDiscipline, ClockDiscipline,
|
|
114
114
|
EnvDiscipline, ConstructorDiscipline, NoClassMethodLogic), and shape (NoComments,
|
|
115
|
-
NoBooleanFlag, NoHashAsObject, ProsePlacement).
|
|
115
|
+
NoBooleanFlag, NoHashAsObject, ProsePlacement). The rubocop-kata command reports
|
|
116
|
+
dead configuration (doctor), buckets a backlog into the stages that cause each other
|
|
117
|
+
(plan), and prints an agent skill for working it (skill).
|
|
116
118
|
email:
|
|
117
119
|
- giaco@hey.com
|
|
118
120
|
executables:
|
|
@@ -148,8 +150,11 @@ files:
|
|
|
148
150
|
- lib/rubocop/cop/kata/real_words.rb
|
|
149
151
|
- lib/rubocop/kata/checkup.rb
|
|
150
152
|
- lib/rubocop/kata/dictionary.rb
|
|
153
|
+
- lib/rubocop/kata/kit.rb
|
|
154
|
+
- lib/rubocop/kata/plan.rb
|
|
151
155
|
- lib/rubocop/kata/plugin.rb
|
|
152
156
|
- lib/rubocop/kata/version.rb
|
|
157
|
+
- skills/rubocop-kata/SKILL.md
|
|
153
158
|
homepage: https://github.com/giacope/rubocop-kata
|
|
154
159
|
licenses:
|
|
155
160
|
- MIT
|