rubocop-kata 0.6.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 +82 -1
- data/README.md +113 -8
- data/config/default.yml +120 -5
- data/exe/rubocop-kata +14 -0
- data/lib/rubocop/cop/kata/agent_noun.rb +30 -3
- data/lib/rubocop/cop/kata/builder_noun.rb +3 -1
- data/lib/rubocop/cop/kata/env_discipline.rb +5 -0
- data/lib/rubocop/cop/kata/real_words.rb +1 -10
- data/lib/rubocop/kata/checkup.rb +73 -0
- data/lib/rubocop/kata/dictionary.rb +13 -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/lib/rubocop-kata.rb +1 -0
- data/skills/rubocop-kata/SKILL.md +126 -0
- metadata +13 -4
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,6 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
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
|
+
|
|
65
|
+
## [0.7.0] - 2026-08-16
|
|
66
|
+
|
|
67
|
+
- New `rubocop-kata doctor` command: reports `.rubocop.yml` entries that
|
|
68
|
+
configure a cop the inherited configuration already disables, and
|
|
69
|
+
`rubocop:disable`/`enable` comments naming a cop that is not enabled where
|
|
70
|
+
the comment sits. RuboCop reports neither. Exits non-zero when it finds
|
|
71
|
+
something.
|
|
72
|
+
- `Style/DisableCopsWithinSourceCodeDirective` is now on.
|
|
73
|
+
- `Kata/AgentNoun` suggests a name when it can derive one: a compound minus its
|
|
74
|
+
agent word (`PaymentProcessor` -> `Payment`), or a regular
|
|
75
|
+
-ator/-ector/-isor/-izer noun the shipped dictionary confirms (`Selector` ->
|
|
76
|
+
`Selection`, `Synthesizer` -> `Synthesis`). It never proposes a name it would
|
|
77
|
+
flag in turn.
|
|
78
|
+
- `Kata/AgentNoun` `AllowedNames` entries now match a whole name or a trailing
|
|
79
|
+
segment of one, so the default `Error` also exempts `UsageError` and
|
|
80
|
+
`ParseError` without listing them.
|
|
81
|
+
- `Kata/BuilderNoun` gained `AllowedNames`, for the method whose name an
|
|
82
|
+
external API dictates (`get_callbacks`).
|
|
83
|
+
- The dictionary moved to `RuboCop::Kata::Dictionary`, shared by
|
|
84
|
+
`Kata/RealWords` and `Kata/AgentNoun`.
|
|
4
85
|
|
|
5
86
|
## [0.6.0] - 2026-08-15
|
|
6
87
|
|
data/README.md
CHANGED
|
@@ -17,13 +17,17 @@ that dodge with a shipped dictionary instead of a word list you maintain:
|
|
|
17
17
|
- **Fifteen house cops.** Naming, dependency discipline, and data honesty in the Elegant Objects spirit — from `Kata/AgentNoun` to `Kata/ClockDiscipline`.
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
class PaymentProcessor # Kata/AgentNoun: `PaymentProcessor` names a doer;
|
|
21
|
-
end #
|
|
20
|
+
class PaymentProcessor # Kata/AgentNoun: `PaymentProcessor` names a doer; name the class
|
|
21
|
+
end # for the thing it is, not the work it does. Try `Payment`.
|
|
22
22
|
|
|
23
23
|
class Payment # OK
|
|
24
24
|
end
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
It suggests a name only when it can derive one: a compound minus its agent word,
|
|
28
|
+
or a regular `-ator`/`-ector`/`-isor`/`-izer` noun the shipped dictionary
|
|
29
|
+
confirms (`Selector` → `Selection`, `Synthesizer` → `Synthesis`).
|
|
30
|
+
|
|
27
31
|
## Getting started
|
|
28
32
|
|
|
29
33
|
Add the gem to your `Gemfile`:
|
|
@@ -49,32 +53,133 @@ bundle exec rubocop
|
|
|
49
53
|
|
|
50
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.
|
|
51
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
|
+
|
|
52
101
|
## The cops
|
|
53
102
|
|
|
54
103
|
| Cop | Default | What it enforces |
|
|
55
104
|
| --- | --- | --- |
|
|
56
|
-
| `Kata/AgentNoun` | on | Classes named for what they are, not `-er`/`-or` doers.
|
|
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`. |
|
|
57
106
|
| `Kata/NoComments` | on | No prose comments; say it in the code. Magic comments, linter directives, and licence headers survive. Autocorrects. |
|
|
58
|
-
| `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. |
|
|
59
108
|
| `Kata/ProsePlacement` | off | Sentence-length strings belong in the presentation layer. Enable with an `Include`/`Exclude` matching your layering. |
|
|
60
109
|
| `Kata/NoUtilName` | on | No junk-drawer names (`Util`, `Helper`, `Manager`, `Service`, …). Tune via `BannedNames`. |
|
|
61
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`. |
|
|
62
111
|
| `Kata/GoodMethodName` | on | One word per method name; a second word needs a role prefix (`after_fork`), a role suffix (`file_of`), or a reviewed `Terms` entry. Tune via `MaxWords`/`Prefixes`/`Suffixes`/`Terms`/`AllowedNames`. |
|
|
63
112
|
| `Kata/GoodVariableName` | on | The same rule for locals, parameters, ivars, class variables, and globals; `_name` and `@_name` stay exempt. |
|
|
64
|
-
| `Kata/BuilderNoun` | on | Builders named for what they return: `total`, not `calculate_total`. Tune via `BannedPrefixes`. |
|
|
113
|
+
| `Kata/BuilderNoun` | on | Builders named for what they return: `total`, not `calculate_total`. Tune via `BannedPrefixes`/`AllowedNames`. |
|
|
65
114
|
| `Kata/NoBooleanFlag` | on | No positional boolean arguments; split the method or use a keyword. |
|
|
66
115
|
| `Kata/ConstructorDiscipline` | on | `initialize` assigns, raises, or freezes — never computes. |
|
|
67
116
|
| `Kata/NoClassMethodLogic` | on | Class methods construct (`build`, `parse`, `of`, `from_*`); instances do the work. |
|
|
68
117
|
| `Kata/NoHashAsObject` | on | A hash with `MaxKeys`+ keys (default 4) wants to be an object. Keyword-argument call sites exempt. |
|
|
69
118
|
| `Kata/ClockDiscipline` | on | No bare `Time.now`/`Date.today`/`.current`; inject a clock. |
|
|
70
|
-
| `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. |
|
|
120
|
+
|
|
121
|
+
## Dead configuration
|
|
122
|
+
|
|
123
|
+
`doctor` reports configuration that no longer does anything: a `.rubocop.yml`
|
|
124
|
+
entry for a cop the inherited configuration disables, and a
|
|
125
|
+
`rubocop:disable`/`enable` comment naming a cop that is not enabled where the
|
|
126
|
+
comment sits. RuboCop reports neither.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
bundle exec rubocop-kata doctor # or: doctor path/to/project
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
.rubocop.yml:20: `Elegant/GoodMethodName` is disabled by the configuration this project inherits; the entry does nothing.
|
|
134
|
+
lib/registry.rb:44: the directive names `Elegant/GoodMethodName`, which is not enabled here; the comment does nothing.
|
|
135
|
+
2 dead entries
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It exits non-zero when it finds something, so it can gate CI.
|
|
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.
|
|
71
175
|
|
|
72
176
|
## The defaults
|
|
73
177
|
|
|
74
178
|
Double-quoted strings, `Metrics/MethodLength: 5`, `Metrics/ClassLength: 100`,
|
|
75
179
|
`Metrics/ParameterLists: 4`, 120-column lines, endless methods on one line,
|
|
76
|
-
`rescue => error`, `NewCops: enable`,
|
|
77
|
-
|
|
180
|
+
`rescue => error`, `NewCops: enable`, heredocs counted as one line in spec
|
|
181
|
+
examples, and no inline `rubocop:disable` comments
|
|
182
|
+
(`Style/DisableCopsWithinSourceCodeDirective`). See
|
|
78
183
|
[config/default.yml](config/default.yml).
|
|
79
184
|
|
|
80
185
|
## License
|
data/config/default.yml
CHANGED
|
@@ -11,19 +11,31 @@ AllCops:
|
|
|
11
11
|
- "vendor/**/*"
|
|
12
12
|
|
|
13
13
|
Kata/AgentNoun:
|
|
14
|
-
Description:
|
|
14
|
+
Description: >-
|
|
15
|
+
Name classes for the thing they are, not the work they do. `AllowedNames`
|
|
16
|
+
entries match a whole name or a trailing segment of one, so `Error` also
|
|
17
|
+
exempts `UsageError`. The default list holds the suffixes a framework
|
|
18
|
+
resolves a class by, which are contracts rather than naming choices.
|
|
15
19
|
Enabled: true
|
|
16
20
|
VersionAdded: "0.1"
|
|
21
|
+
VersionChanged: "0.8"
|
|
17
22
|
AllowedNames:
|
|
18
23
|
- Error
|
|
24
|
+
- Controller
|
|
25
|
+
- Mailer
|
|
26
|
+
- Serializer
|
|
19
27
|
|
|
20
28
|
Kata/NoComments:
|
|
21
29
|
Description: "Say it in the code; keep only comments the machine or the law reads."
|
|
22
30
|
Enabled: true
|
|
23
31
|
VersionAdded: "0.1"
|
|
32
|
+
VersionChanged: "0.8"
|
|
24
33
|
Exclude:
|
|
25
34
|
- "spec/**/*"
|
|
35
|
+
- "test/**/*"
|
|
26
36
|
- "*.gemspec"
|
|
37
|
+
- "Gemfile"
|
|
38
|
+
- "**/Gemfile"
|
|
27
39
|
|
|
28
40
|
Kata/GoodMethodName:
|
|
29
41
|
Description: >-
|
|
@@ -141,11 +153,18 @@ Kata/IoDiscipline:
|
|
|
141
153
|
Description: "Write through an injected io, not bare puts/warn/pp/p."
|
|
142
154
|
Enabled: true
|
|
143
155
|
VersionAdded: "0.1"
|
|
156
|
+
VersionChanged: "0.8"
|
|
144
157
|
Exclude:
|
|
145
158
|
- "spec/**/*"
|
|
159
|
+
- "test/**/*"
|
|
146
160
|
- "bin/**/*"
|
|
147
161
|
- "exe/**/*"
|
|
162
|
+
- "config/**/*"
|
|
163
|
+
- "db/seeds.rb"
|
|
164
|
+
- "db/seeds/**/*"
|
|
165
|
+
- "lib/tasks/**/*"
|
|
148
166
|
- "Rakefile"
|
|
167
|
+
- "**/*.rake"
|
|
149
168
|
- "*.gemspec"
|
|
150
169
|
|
|
151
170
|
Kata/ProsePlacement:
|
|
@@ -202,12 +221,14 @@ Kata/BuilderNoun:
|
|
|
202
221
|
Description: "Builders are named for what they return; verbs are for side effects."
|
|
203
222
|
Enabled: true
|
|
204
223
|
VersionAdded: "0.3"
|
|
224
|
+
VersionChanged: "0.7"
|
|
205
225
|
BannedPrefixes:
|
|
206
226
|
- get
|
|
207
227
|
- calculate
|
|
208
228
|
- compute
|
|
209
229
|
- retrieve
|
|
210
230
|
- generate
|
|
231
|
+
AllowedNames: []
|
|
211
232
|
|
|
212
233
|
Kata/NoBooleanFlag:
|
|
213
234
|
Description: "A boolean flag is two methods trapped in one."
|
|
@@ -215,6 +236,7 @@ Kata/NoBooleanFlag:
|
|
|
215
236
|
VersionAdded: "0.3"
|
|
216
237
|
Exclude:
|
|
217
238
|
- "spec/**/*"
|
|
239
|
+
- "test/**/*"
|
|
218
240
|
|
|
219
241
|
Kata/ConstructorDiscipline:
|
|
220
242
|
Description: "Constructors assign; they do not compute."
|
|
@@ -222,6 +244,7 @@ Kata/ConstructorDiscipline:
|
|
|
222
244
|
VersionAdded: "0.3"
|
|
223
245
|
Exclude:
|
|
224
246
|
- "spec/**/*"
|
|
247
|
+
- "test/**/*"
|
|
225
248
|
|
|
226
249
|
Kata/NoClassMethodLogic:
|
|
227
250
|
Description: "Classes construct; instances work."
|
|
@@ -229,6 +252,7 @@ Kata/NoClassMethodLogic:
|
|
|
229
252
|
VersionAdded: "0.3"
|
|
230
253
|
Exclude:
|
|
231
254
|
- "spec/**/*"
|
|
255
|
+
- "test/**/*"
|
|
232
256
|
AllowedNames:
|
|
233
257
|
- build
|
|
234
258
|
- parse
|
|
@@ -241,27 +265,43 @@ Kata/NoHashAsObject:
|
|
|
241
265
|
MaxKeys: 4
|
|
242
266
|
Exclude:
|
|
243
267
|
- "spec/**/*"
|
|
268
|
+
- "test/**/*"
|
|
244
269
|
- "config/**/*"
|
|
245
270
|
|
|
246
271
|
Kata/ClockDiscipline:
|
|
247
272
|
Description: "Read time from an injected clock, not the ambient one."
|
|
248
273
|
Enabled: true
|
|
249
274
|
VersionAdded: "0.3"
|
|
275
|
+
VersionChanged: "0.8"
|
|
250
276
|
Exclude:
|
|
251
277
|
- "spec/**/*"
|
|
278
|
+
- "test/**/*"
|
|
252
279
|
- "bin/**/*"
|
|
253
280
|
- "exe/**/*"
|
|
281
|
+
- "config/**/*"
|
|
282
|
+
- "db/seeds.rb"
|
|
283
|
+
- "db/seeds/**/*"
|
|
284
|
+
- "lib/tasks/**/*"
|
|
285
|
+
- "Rakefile"
|
|
286
|
+
- "**/*.rake"
|
|
287
|
+
- "*.gemspec"
|
|
254
288
|
|
|
255
289
|
Kata/EnvDiscipline:
|
|
256
290
|
Description: "ENV belongs in the boot layer; pass configuration in."
|
|
257
291
|
Enabled: true
|
|
258
292
|
VersionAdded: "0.3"
|
|
293
|
+
VersionChanged: "0.8"
|
|
259
294
|
Exclude:
|
|
260
295
|
- "spec/**/*"
|
|
261
|
-
- "
|
|
296
|
+
- "test/**/*"
|
|
262
297
|
- "bin/**/*"
|
|
263
298
|
- "exe/**/*"
|
|
299
|
+
- "config/**/*"
|
|
300
|
+
- "db/seeds.rb"
|
|
301
|
+
- "db/seeds/**/*"
|
|
302
|
+
- "lib/tasks/**/*"
|
|
264
303
|
- "Rakefile"
|
|
304
|
+
- "**/*.rake"
|
|
265
305
|
- "*.gemspec"
|
|
266
306
|
|
|
267
307
|
Style/Documentation:
|
|
@@ -308,26 +348,42 @@ Layout/LineEndStringConcatenationIndentation:
|
|
|
308
348
|
Metrics/BlockLength:
|
|
309
349
|
Exclude:
|
|
310
350
|
- "spec/**/*"
|
|
351
|
+
- "test/**/*"
|
|
311
352
|
- "*.gemspec"
|
|
312
353
|
|
|
313
354
|
Metrics/ClassLength:
|
|
314
355
|
Max: 100
|
|
356
|
+
Exclude:
|
|
357
|
+
- "spec/**/*"
|
|
358
|
+
- "test/**/*"
|
|
359
|
+
- "**/*_test.rb"
|
|
315
360
|
|
|
316
361
|
Metrics/ModuleLength:
|
|
317
362
|
Max: 100
|
|
318
363
|
Exclude:
|
|
319
364
|
- "spec/**/*"
|
|
365
|
+
- "test/**/*"
|
|
366
|
+
- "**/*_test.rb"
|
|
320
367
|
|
|
368
|
+
# A wrapped call reads as one line, so it costs one; otherwise Max and
|
|
369
|
+
# Layout/LineLength cannot both be satisfied.
|
|
321
370
|
Metrics/MethodLength:
|
|
322
371
|
Max: 5
|
|
372
|
+
CountAsOne:
|
|
373
|
+
- array
|
|
374
|
+
- hash
|
|
375
|
+
- heredoc
|
|
376
|
+
- method_call
|
|
323
377
|
Exclude:
|
|
324
378
|
- "spec/**/*"
|
|
379
|
+
- "test/**/*"
|
|
325
380
|
|
|
326
381
|
Metrics/ParameterLists:
|
|
327
382
|
Max: 4
|
|
328
383
|
CountKeywordArgs: true
|
|
329
384
|
Exclude:
|
|
330
385
|
- "spec/**/*"
|
|
386
|
+
- "test/**/*"
|
|
331
387
|
|
|
332
388
|
RSpec/ExampleLength:
|
|
333
389
|
CountAsOne:
|
|
@@ -339,10 +395,12 @@ RSpec/VerifiedDoubles:
|
|
|
339
395
|
Elegant/NoEmptyLinesInBlocks:
|
|
340
396
|
Exclude:
|
|
341
397
|
- "spec/**/*"
|
|
398
|
+
- "test/**/*"
|
|
342
399
|
|
|
343
400
|
Elegant/NoEmptyLinesInMethods:
|
|
344
401
|
Exclude:
|
|
345
402
|
- "spec/**/*"
|
|
403
|
+
- "test/**/*"
|
|
346
404
|
|
|
347
405
|
Elegant/NoComments:
|
|
348
406
|
Enabled: false
|
|
@@ -364,6 +422,16 @@ Layout/ClosingParenthesisIndentation:
|
|
|
364
422
|
Layout/FirstArgumentIndentation:
|
|
365
423
|
Enabled: false
|
|
366
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
|
+
|
|
367
435
|
Layout/MultilineMethodCallBraceLayout:
|
|
368
436
|
Enabled: false
|
|
369
437
|
|
|
@@ -379,21 +447,32 @@ Layout/EmptyLinesAroundAccessModifier:
|
|
|
379
447
|
Layout/EmptyLinesAfterModuleInclusion:
|
|
380
448
|
Enabled: false
|
|
381
449
|
|
|
450
|
+
# --- A disable comment is a note saying what the code should have said;
|
|
451
|
+
# `rubocop-kata doctor` finds the ones core cannot see.
|
|
452
|
+
Style/DisableCopsWithinSourceCodeDirective:
|
|
453
|
+
Enabled: true
|
|
454
|
+
|
|
382
455
|
# --- Correctors known to rewrite code incorrectly; report only.
|
|
383
456
|
# Inlines a variable past side effects (ivar resets, later statements) and
|
|
384
457
|
# corrupts Ruby 3.1 keyword shorthand.
|
|
385
458
|
Elegant/NoRedundantVariable:
|
|
386
459
|
AutoCorrect: false
|
|
387
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
|
+
|
|
388
467
|
# Rewrites reopened classes into modules, clashing with the primary
|
|
389
468
|
# class definition at boot.
|
|
390
469
|
Style/StaticClass:
|
|
391
470
|
AutoCorrect: false
|
|
392
471
|
|
|
393
|
-
#
|
|
394
|
-
#
|
|
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.
|
|
395
474
|
Lint/NumberConversion:
|
|
396
|
-
|
|
475
|
+
Enabled: false
|
|
397
476
|
|
|
398
477
|
# Alphabetizing requires breaks load-order-dependent boot sequences.
|
|
399
478
|
Style/RequireOrder:
|
|
@@ -402,3 +481,39 @@ Style/RequireOrder:
|
|
|
402
481
|
ThreadSafety/DirChdir:
|
|
403
482
|
Exclude:
|
|
404
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
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rubocop-kata"
|
|
5
|
+
require "rubocop/kata/checkup"
|
|
6
|
+
require "rubocop/kata/plan"
|
|
7
|
+
require "rubocop/kata/kit"
|
|
8
|
+
|
|
9
|
+
COMMANDS = { "doctor" => RuboCop::Kata::Checkup, "plan" => RuboCop::Kata::Plan, "skill" => RuboCop::Kata::Kit }.freeze
|
|
10
|
+
|
|
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)
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
class RuboCop::Cop::Kata::AgentNoun < RuboCop::Cop::Base
|
|
4
4
|
MSG = "`%s` names a doer; name the class for the thing it is, not the work it does."
|
|
5
|
-
|
|
5
|
+
HINT = "#{MSG} Try `%s`.".freeze
|
|
6
6
|
SUFFIX = /(?:er|or)\z/
|
|
7
|
+
SEGMENT = /[A-Z]+(?=[A-Z][a-z]|\z)|[A-Z][a-z0-9]*/
|
|
8
|
+
DERIVATIONS = [[/ator\z/, "ation"], [/ector\z/, "ection"], [/isor\z/, "ision"], [/i[zs]er\z/, "is"]].freeze
|
|
7
9
|
|
|
8
10
|
def on_class(node) = check(node)
|
|
9
11
|
|
|
@@ -15,8 +17,33 @@ class RuboCop::Cop::Kata::AgentNoun < RuboCop::Cop::Base
|
|
|
15
17
|
name = node.identifier.short_name.to_s
|
|
16
18
|
return unless SUFFIX.match?(name)
|
|
17
19
|
return if allowed?(name)
|
|
18
|
-
add_offense(node.identifier, message:
|
|
20
|
+
add_offense(node.identifier, message: message(name))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def message(name)
|
|
24
|
+
hint = suggestion(name)
|
|
25
|
+
hint ? format(HINT, name, hint) : format(MSG, name)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
def
|
|
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)
|
|
34
|
+
segments = name.scan(SEGMENT)
|
|
35
|
+
[segments[0..-2].join, segments.last.to_s]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def proposal(name)
|
|
39
|
+
return if name.empty? || SUFFIX.match?(name)
|
|
40
|
+
return unless known?(name)
|
|
41
|
+
name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def known?(name) = RuboCop::Kata::Dictionary::ENTRIES.include?(name.scan(SEGMENT).last.to_s.downcase)
|
|
45
|
+
|
|
46
|
+
def allowed?(name) = list.any? { name.end_with?(it) }
|
|
47
|
+
|
|
48
|
+
def list = Array(cop_config["AllowedNames"]).map(&:to_s).reject(&:empty?)
|
|
22
49
|
end
|
|
@@ -12,10 +12,12 @@ class RuboCop::Cop::Kata::BuilderNoun < RuboCop::Cop::Base
|
|
|
12
12
|
def check(node)
|
|
13
13
|
name = node.method_name.to_s
|
|
14
14
|
verb = prefix(name)
|
|
15
|
-
return
|
|
15
|
+
return if verb.nil? || allowed?(name)
|
|
16
16
|
add_offense(node.loc.name, message: format(MSG, name.delete_prefix("#{verb}_"), name))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def allowed?(name) = Array(cop_config["AllowedNames"]).map(&:to_s).include?(name)
|
|
20
|
+
|
|
19
21
|
def prefix(name)
|
|
20
22
|
Array(cop_config["BannedPrefixes"]).map(&:to_s).find { name.start_with?("#{it}_") }
|
|
21
23
|
end
|
|
@@ -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
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "zlib"
|
|
4
|
-
|
|
5
3
|
class RuboCop::Cop::Kata::RealWords < RuboCop::Cop::Base
|
|
6
4
|
MSG = "%s is not in the dictionary — restore the underscore between smashed words, spell the " \
|
|
7
5
|
"abbreviation out, or add it to `Terms` if it is one domain term here."
|
|
8
6
|
ABBREVIATION_MSG = "%s is an abbreviation; spell the word out."
|
|
9
|
-
WORDS = File.expand_path("../../../../data/words.txt.gz", __dir__)
|
|
10
|
-
SOFTWARE = File.expand_path("../../../../data/software.txt.gz", __dir__)
|
|
11
|
-
EXTRA = File.expand_path("../../../../data/supplement.txt", __dir__)
|
|
12
|
-
DICTIONARY = Set.new(
|
|
13
|
-
[WORDS, SOFTWARE].flat_map { Zlib.gunzip(File.binread(it)).split("\n") } +
|
|
14
|
-
File.readlines(EXTRA, chomp: true)
|
|
15
|
-
).freeze
|
|
16
7
|
SIGIL = /\A(@@|@|\$)/
|
|
17
8
|
DERIVATIONS = [
|
|
18
9
|
[/s\z/, ""], [/es\z/, ""], [/ies\z/, "y"],
|
|
@@ -63,7 +54,7 @@ class RuboCop::Cop::Kata::RealWords < RuboCop::Cop::Base
|
|
|
63
54
|
|
|
64
55
|
def word?(segment) = segment.length < 2 || term?(segment) || (known?(segment) && !banned?(segment))
|
|
65
56
|
|
|
66
|
-
def known?(segment) = forms(segment).any? {
|
|
57
|
+
def known?(segment) = forms(segment).any? { RuboCop::Kata::Dictionary::ENTRIES.include?(it) }
|
|
67
58
|
|
|
68
59
|
def forms(segment)
|
|
69
60
|
base = segment.sub(/\d+\z/, "")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
class RuboCop::Kata::Checkup
|
|
6
|
+
INERT = "%s:%d: `%s` is disabled by the configuration this project inherits; the entry does nothing."
|
|
7
|
+
DEAD = "%s:%d: the directive names `%s`, which is not enabled here; the comment does nothing."
|
|
8
|
+
TALLY = "%d dead entr%s"
|
|
9
|
+
KEY = %r{\A([A-Z]\w*/\w+):}
|
|
10
|
+
|
|
11
|
+
def initialize(root, io: $stdout)
|
|
12
|
+
@root = root
|
|
13
|
+
@io = io
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
found = inert + dead
|
|
18
|
+
found.each { @io.puts(it) }
|
|
19
|
+
@io.puts(found.empty? ? "clean" : tally(found))
|
|
20
|
+
found.empty? ? 0 : 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def tally(found) = format(TALLY, found.length, found.one? ? "y" : "ies")
|
|
26
|
+
|
|
27
|
+
def inert
|
|
28
|
+
entries.filter_map { |key, line| format(INERT, file, line, key) unless live?(key) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
lines.each_with_index.filter_map { |text, index| [text[KEY, 1], index + 1] if text.match?(KEY) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def lines = File.file?(file) ? File.readlines(file) : []
|
|
36
|
+
|
|
37
|
+
def file = File.join(@root, ".rubocop.yml")
|
|
38
|
+
|
|
39
|
+
def live?(key) = config.cop_enabled?(key) || off?(key)
|
|
40
|
+
|
|
41
|
+
def off?(key) = project.dig(key, "Enabled") == false
|
|
42
|
+
|
|
43
|
+
def project = @_project ||= (YAML.safe_load_file(file, aliases: true) if File.file?(file)) || {}
|
|
44
|
+
|
|
45
|
+
def dead = targets.flat_map { directives(it) }
|
|
46
|
+
|
|
47
|
+
def targets = RuboCop::TargetFinder.new(store).find([@root], :only_recognized_file_types)
|
|
48
|
+
|
|
49
|
+
def directives(path)
|
|
50
|
+
RuboCop::ProcessedSource.from_file(path, config.target_ruby_version).comments
|
|
51
|
+
.map { RuboCop::DirectiveComment.new(it) }
|
|
52
|
+
.filter_map { finding(path, it) }
|
|
53
|
+
rescue StandardError
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def finding(path, directive)
|
|
58
|
+
return unless directive.start_with_marker? && !directive.all_cops?
|
|
59
|
+
stale = directive.cop_names.reject { active?(it, path) }
|
|
60
|
+
return if stale.empty?
|
|
61
|
+
format(DEAD, path, directive.line_number, stale.join("`, `"))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def active?(name, path)
|
|
65
|
+
configuration = store.for_file(path)
|
|
66
|
+
cop = RuboCop::Cop::Registry.global.find_by_cop_name(name)
|
|
67
|
+
configuration.cop_enabled?(name) && cop&.new(configuration)&.relevant_file?(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def config = @_config ||= store.for_dir(@root)
|
|
71
|
+
|
|
72
|
+
def store = @_store ||= RuboCop::ConfigStore.new
|
|
73
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module RuboCop::Kata::Dictionary
|
|
6
|
+
WORDS = File.expand_path("../../../data/words.txt.gz", __dir__)
|
|
7
|
+
SOFTWARE = File.expand_path("../../../data/software.txt.gz", __dir__)
|
|
8
|
+
EXTRA = File.expand_path("../../../data/supplement.txt", __dir__)
|
|
9
|
+
ENTRIES = Set.new(
|
|
10
|
+
[WORDS, SOFTWARE].flat_map { Zlib.gunzip(File.binread(it)).split("\n") } +
|
|
11
|
+
File.readlines(EXTRA, chomp: true)
|
|
12
|
+
).freeze
|
|
13
|
+
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
data/lib/rubocop-kata.rb
CHANGED
|
@@ -27,5 +27,6 @@ require_relative "rubocop/cop/kata/no_hash_as_object"
|
|
|
27
27
|
require_relative "rubocop/cop/kata/no_util_name"
|
|
28
28
|
require_relative "rubocop/cop/kata/prose_placement"
|
|
29
29
|
require_relative "rubocop/cop/kata/real_words"
|
|
30
|
+
require_relative "rubocop/kata/dictionary"
|
|
30
31
|
require_relative "rubocop/kata/plugin"
|
|
31
32
|
require_relative "rubocop/kata/version"
|
|
@@ -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,11 +1,11 @@
|
|
|
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
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -112,10 +112,13 @@ 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
|
-
executables:
|
|
120
|
+
executables:
|
|
121
|
+
- rubocop-kata
|
|
119
122
|
extensions: []
|
|
120
123
|
extra_rdoc_files: []
|
|
121
124
|
files:
|
|
@@ -128,6 +131,7 @@ files:
|
|
|
128
131
|
- data/software.txt.gz
|
|
129
132
|
- data/supplement.txt
|
|
130
133
|
- data/words.txt.gz
|
|
134
|
+
- exe/rubocop-kata
|
|
131
135
|
- lib/rubocop-kata.rb
|
|
132
136
|
- lib/rubocop/cop/kata/agent_noun.rb
|
|
133
137
|
- lib/rubocop/cop/kata/builder_noun.rb
|
|
@@ -144,8 +148,13 @@ files:
|
|
|
144
148
|
- lib/rubocop/cop/kata/no_util_name.rb
|
|
145
149
|
- lib/rubocop/cop/kata/prose_placement.rb
|
|
146
150
|
- lib/rubocop/cop/kata/real_words.rb
|
|
151
|
+
- lib/rubocop/kata/checkup.rb
|
|
152
|
+
- lib/rubocop/kata/dictionary.rb
|
|
153
|
+
- lib/rubocop/kata/kit.rb
|
|
154
|
+
- lib/rubocop/kata/plan.rb
|
|
147
155
|
- lib/rubocop/kata/plugin.rb
|
|
148
156
|
- lib/rubocop/kata/version.rb
|
|
157
|
+
- skills/rubocop-kata/SKILL.md
|
|
149
158
|
homepage: https://github.com/giacope/rubocop-kata
|
|
150
159
|
licenses:
|
|
151
160
|
- MIT
|