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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d84b32b4beb311cd73ed3af609400cc07a4a1e7fb41258e813feab7a7477ae9
4
- data.tar.gz: 3038bc491f3197c6623f30f68742fda4008c4519cbacc2c95dc36e0ebc10276c
3
+ metadata.gz: da7876f7f43029f7dfb90b0fb9527987fe9c50ee40336d0ba4fffdcc2cbad83f
4
+ data.tar.gz: 902e0c01d3cc5e71553d3c90c68a707b7f3a554cc3e6ec1228f0640ae8772d6a
5
5
  SHA512:
6
- metadata.gz: b38144839ec596e2f646f89f2c28328978cf030d7bbc9c31deccad49fe94ad9775e75a2af69d51961cfed832f3acc7b49c32c43f0830fc98d300c8033cda1230
7
- data.tar.gz: 5ab77b46b9eab5d756368860c1f40866111964217dc0fda9cda532c49574fc6472dc60fb2a091c1e3d54269b63266b956f4bfd9a900ad50528dae96a8a8dd3de
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 specs/scripts; write through an injected `@io` or an explicit receiver. |
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.7"
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
- - "config/**/*"
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
- # Turns lenient `.to_i` into strict Integer(x, 10), which raises on
405
- # nils/Integers call sites legitimately receive.
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
- AutoCorrect: false
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
- abort("usage: rubocop-kata doctor [path]") unless ARGV.first == "doctor"
9
+ COMMANDS = { "doctor" => RuboCop::Kata::Checkup, "plan" => RuboCop::Kata::Plan, "skill" => RuboCop::Kata::Kit }.freeze
8
10
 
9
- exit(RuboCop::Kata::Checkup.new(ARGV.fetch(1, Dir.pwd)).run)
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
- return proposal(segments[0..-2].join) if segments.length > 1
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Kata
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
@@ -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.7.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