rubocop-kata 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30d894b4455161e0ce3dac01fa4c627de974cf89e7d1372a610749ee9ba0ad26
4
- data.tar.gz: 6bfcc2c6b38d212c8ff6ac0efc16bd4b563a74b972ccc546013619b813c0f6e2
3
+ metadata.gz: 4d84b32b4beb311cd73ed3af609400cc07a4a1e7fb41258e813feab7a7477ae9
4
+ data.tar.gz: 3038bc491f3197c6623f30f68742fda4008c4519cbacc2c95dc36e0ebc10276c
5
5
  SHA512:
6
- metadata.gz: f7c5bd992aa75df3b4f38c9182005dc7f346b4ea0db83e5756fb02f638acbc034b32a7146dece44e7c9421ce4d04cd064e02ce46309d6ba4513a8e5560540c5e
7
- data.tar.gz: '08f28bf480cf2fcef885db1d5df0c92d3200a53b898dcc01fc026172214276a5a83a1a2b0b6bc807e2e4aed9ac78439cac6c50b7a53643986e44c05bf7a0e388'
6
+ metadata.gz: b38144839ec596e2f646f89f2c28328978cf030d7bbc9c31deccad49fe94ad9775e75a2af69d51961cfed832f3acc7b49c32c43f0830fc98d300c8033cda1230
7
+ data.tar.gz: 5ab77b46b9eab5d756368860c1f40866111964217dc0fda9cda532c49574fc6472dc60fb2a091c1e3d54269b63266b956f4bfd9a900ad50528dae96a8a8dd3de
data/CHANGELOG.md CHANGED
@@ -1,6 +1,25 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.7.0] - 2026-08-16
4
+
5
+ - New `rubocop-kata doctor` command: reports `.rubocop.yml` entries that
6
+ configure a cop the inherited configuration already disables, and
7
+ `rubocop:disable`/`enable` comments naming a cop that is not enabled where
8
+ the comment sits. RuboCop reports neither. Exits non-zero when it finds
9
+ something.
10
+ - `Style/DisableCopsWithinSourceCodeDirective` is now on.
11
+ - `Kata/AgentNoun` suggests a name when it can derive one: a compound minus its
12
+ agent word (`PaymentProcessor` -> `Payment`), or a regular
13
+ -ator/-ector/-isor/-izer noun the shipped dictionary confirms (`Selector` ->
14
+ `Selection`, `Synthesizer` -> `Synthesis`). It never proposes a name it would
15
+ flag in turn.
16
+ - `Kata/AgentNoun` `AllowedNames` entries now match a whole name or a trailing
17
+ segment of one, so the default `Error` also exempts `UsageError` and
18
+ `ParseError` without listing them.
19
+ - `Kata/BuilderNoun` gained `AllowedNames`, for the method whose name an
20
+ external API dictates (`get_callbacks`).
21
+ - The dictionary moved to `RuboCop::Kata::Dictionary`, shared by
22
+ `Kata/RealWords` and `Kata/AgentNoun`.
4
23
 
5
24
  ## [0.6.0] - 2026-08-15
6
25
 
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 # name the class for the thing it is, not the work it does.
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`:
@@ -53,7 +57,7 @@ That's it. The plugin loads the bundled extensions and the shared defaults, so y
53
57
 
54
58
  | Cop | Default | What it enforces |
55
59
  | --- | --- | --- |
56
- | `Kata/AgentNoun` | on | Classes named for what they are, not `-er`/`-or` doers. Allow exceptions via `AllowedNames`. |
60
+ | `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
61
  | `Kata/NoComments` | on | No prose comments; say it in the code. Magic comments, linter directives, and licence headers survive. Autocorrects. |
58
62
  | `Kata/IoDiscipline` | on | No bare `puts`/`warn`/`pp`/`p` outside specs/scripts; write through an injected `@io` or an explicit receiver. |
59
63
  | `Kata/ProsePlacement` | off | Sentence-length strings belong in the presentation layer. Enable with an `Include`/`Exclude` matching your layering. |
@@ -61,7 +65,7 @@ That's it. The plugin loads the bundled extensions and the shared defaults, so y
61
65
  | `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
66
  | `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
67
  | `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`. |
68
+ | `Kata/BuilderNoun` | on | Builders named for what they return: `total`, not `calculate_total`. Tune via `BannedPrefixes`/`AllowedNames`. |
65
69
  | `Kata/NoBooleanFlag` | on | No positional boolean arguments; split the method or use a keyword. |
66
70
  | `Kata/ConstructorDiscipline` | on | `initialize` assigns, raises, or freezes — never computes. |
67
71
  | `Kata/NoClassMethodLogic` | on | Class methods construct (`build`, `parse`, `of`, `from_*`); instances do the work. |
@@ -69,12 +73,32 @@ That's it. The plugin loads the bundled extensions and the shared defaults, so y
69
73
  | `Kata/ClockDiscipline` | on | No bare `Time.now`/`Date.today`/`.current`; inject a clock. |
70
74
  | `Kata/EnvDiscipline` | on | `ENV` reads only in the boot layer (`config/`, `bin/`, `exe/`). |
71
75
 
76
+ ## Dead configuration
77
+
78
+ `doctor` reports configuration that no longer does anything: a `.rubocop.yml`
79
+ entry for a cop the inherited configuration disables, and a
80
+ `rubocop:disable`/`enable` comment naming a cop that is not enabled where the
81
+ comment sits. RuboCop reports neither.
82
+
83
+ ```sh
84
+ bundle exec rubocop-kata doctor # or: doctor path/to/project
85
+ ```
86
+
87
+ ```
88
+ .rubocop.yml:20: `Elegant/GoodMethodName` is disabled by the configuration this project inherits; the entry does nothing.
89
+ lib/registry.rb:44: the directive names `Elegant/GoodMethodName`, which is not enabled here; the comment does nothing.
90
+ 2 dead entries
91
+ ```
92
+
93
+ It exits non-zero when it finds something, so it can gate CI.
94
+
72
95
  ## The defaults
73
96
 
74
97
  Double-quoted strings, `Metrics/MethodLength: 5`, `Metrics/ClassLength: 100`,
75
98
  `Metrics/ParameterLists: 4`, 120-column lines, endless methods on one line,
76
- `rescue => error`, `NewCops: enable`, and heredocs counted as one line in
77
- spec examples. See
99
+ `rescue => error`, `NewCops: enable`, heredocs counted as one line in spec
100
+ examples, and no inline `rubocop:disable` comments
101
+ (`Style/DisableCopsWithinSourceCodeDirective`). See
78
102
  [config/default.yml](config/default.yml).
79
103
 
80
104
  ## License
data/config/default.yml CHANGED
@@ -11,9 +11,13 @@ AllCops:
11
11
  - "vendor/**/*"
12
12
 
13
13
  Kata/AgentNoun:
14
- Description: "Name classes for the thing they are, not the work they do."
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`.
15
18
  Enabled: true
16
19
  VersionAdded: "0.1"
20
+ VersionChanged: "0.7"
17
21
  AllowedNames:
18
22
  - Error
19
23
 
@@ -202,12 +206,14 @@ Kata/BuilderNoun:
202
206
  Description: "Builders are named for what they return; verbs are for side effects."
203
207
  Enabled: true
204
208
  VersionAdded: "0.3"
209
+ VersionChanged: "0.7"
205
210
  BannedPrefixes:
206
211
  - get
207
212
  - calculate
208
213
  - compute
209
214
  - retrieve
210
215
  - generate
216
+ AllowedNames: []
211
217
 
212
218
  Kata/NoBooleanFlag:
213
219
  Description: "A boolean flag is two methods trapped in one."
@@ -379,6 +385,11 @@ Layout/EmptyLinesAroundAccessModifier:
379
385
  Layout/EmptyLinesAfterModuleInclusion:
380
386
  Enabled: false
381
387
 
388
+ # --- A disable comment is a note saying what the code should have said;
389
+ # `rubocop-kata doctor` finds the ones core cannot see.
390
+ Style/DisableCopsWithinSourceCodeDirective:
391
+ Enabled: true
392
+
382
393
  # --- Correctors known to rewrite code incorrectly; report only.
383
394
  # Inlines a variable past side effects (ivar resets, later statements) and
384
395
  # corrupts Ruby 3.1 keyword shorthand.
data/exe/rubocop-kata ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubocop-kata"
5
+ require "rubocop/kata/checkup"
6
+
7
+ abort("usage: rubocop-kata doctor [path]") unless ARGV.first == "doctor"
8
+
9
+ exit(RuboCop::Kata::Checkup.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,29 @@ 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: format(MSG, name))
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)
26
+ end
27
+
28
+ def suggestion(name)
29
+ 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
19
32
  end
20
33
 
21
- def allowed?(name) = Array(cop_config["AllowedNames"]).map(&:to_s).include?(name)
34
+ def proposal(name)
35
+ return if name.empty? || SUFFIX.match?(name)
36
+ return unless known?(name)
37
+ name
38
+ end
39
+
40
+ def known?(name) = RuboCop::Kata::Dictionary::ENTRIES.include?(name.scan(SEGMENT).last.to_s.downcase)
41
+
42
+ def allowed?(name) = list.any? { name.end_with?(it) }
43
+
44
+ def list = Array(cop_config["AllowedNames"]).map(&:to_s).reject(&:empty?)
22
45
  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 unless verb
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
@@ -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? { DICTIONARY.include?(it) }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Kata
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
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"
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -115,7 +115,8 @@ description: Bundles a curated RuboCop stack (rspec, performance, elegant, packa
115
115
  NoBooleanFlag, NoHashAsObject, ProsePlacement).
116
116
  email:
117
117
  - giaco@hey.com
118
- executables: []
118
+ executables:
119
+ - rubocop-kata
119
120
  extensions: []
120
121
  extra_rdoc_files: []
121
122
  files:
@@ -128,6 +129,7 @@ files:
128
129
  - data/software.txt.gz
129
130
  - data/supplement.txt
130
131
  - data/words.txt.gz
132
+ - exe/rubocop-kata
131
133
  - lib/rubocop-kata.rb
132
134
  - lib/rubocop/cop/kata/agent_noun.rb
133
135
  - lib/rubocop/cop/kata/builder_noun.rb
@@ -144,6 +146,8 @@ files:
144
146
  - lib/rubocop/cop/kata/no_util_name.rb
145
147
  - lib/rubocop/cop/kata/prose_placement.rb
146
148
  - lib/rubocop/cop/kata/real_words.rb
149
+ - lib/rubocop/kata/checkup.rb
150
+ - lib/rubocop/kata/dictionary.rb
147
151
  - lib/rubocop/kata/plugin.rb
148
152
  - lib/rubocop/kata/version.rb
149
153
  homepage: https://github.com/giacope/rubocop-kata