kaizo 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: 7a167afcda94e477c2fd351f0f88c4c4f69b4adb987e0e74a59f50c516aa6080
4
- data.tar.gz: f002c0b1464262edc8eb988f046b935fb9347d2258585c143acc6dd46911f86b
3
+ metadata.gz: a33b49134e0a0a871e9b6358cf3fcc9ab88d9ebaca5e8928003de08594c01a11
4
+ data.tar.gz: 7ed8c9d6780bc64c233b94135850694da8b2bfa1e314dd7369dddf7560f7d42d
5
5
  SHA512:
6
- metadata.gz: 41ec7ca0e3f99dc13c761540b9ac7c06bd66f1cbcdecf776db8f1dd4dcd0c412008ae70f6ac73e6d9295662a27d70b3c2b2973930674ea7229fc1d173b4bb33e
7
- data.tar.gz: f3ea240a663c70959adf03f95c5b49452ae3ae71a93818b8dcdedcf8bf1b72f10acd9d1db95494e12f7ee592a3162b8590a5d99603caca710126c810147bdaca
6
+ metadata.gz: b4c8a8645f32a48c82b52d23966d37d2608c63a0c84e4820e33c15ed6f58bb84c40658eef9de990874e3e0d304c8d6d27abec8ef5f708ff3468f6430663cdd12
7
+ data.tar.gz: f4189d76f5bb36959f71c84f0497b33805e77ae4e8b04941c78e8517a9012f92447dbbb08c8063b5738746018330fba474abcee324656461af7ec83f2946a96a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Add `Kaizo/PluralCollectionName` cop: flags a method that returns an array
6
+ under a singular name (`def user` handing back `[first, second]`), since a
7
+ plural name documents what the caller gets. Ruby has no return types, so the
8
+ detection is a heuristic and errs toward silence — a method is flagged only
9
+ when every value it can return is unambiguously an array, so one branch
10
+ returning `nil` keeps it quiet. `ArrayMethods` covers calls whose result is an
11
+ `Array` whatever the receiver; `select` and `reject` are excluded on purpose,
12
+ since on a `Hash` they return a `Hash`. Predicate, writer, and operator
13
+ methods are exempt, as is `initialize`. `IrregularPlurals` handles plurals
14
+ without a trailing `s`. No autocorrection. Enabled by default.
15
+ - The argument-counting cops (`Kaizo/PositionalArguments`, `Kaizo/TotalArguments`,
16
+ `Kaizo/KeywordArguments`) no longer flag **operator methods**. The arity of `[]=`
17
+ is fixed by Ruby's syntax — an index (or indices) plus the assigned value — so it
18
+ cannot be modeled away; the same holds for `[]`, `<=>`, `+`, `<<`, `==`, and the
19
+ rest of the operator family. Covers `def`, `def self.`, and the
20
+ `define_method(:[]=)` form; a `define_method` whose name is computed at runtime is
21
+ still checked, since the name cannot be known statically. This joins the existing
22
+ `Struct.new`/`Data.define` `initialize` exemption. Ordinary writers
23
+ (`def name=(value)`) are unaffected.
24
+ - The Public Config now ships `Style/HashSyntax` with
25
+ `EnforcedShorthandSyntax: always`, preferring Ruby 3.1's hash-value shorthand
26
+ (`Session.new(table:)` over `Session.new(table: table)`). Override it in your own
27
+ config if you want the explicit form. *(Landed on `master` after 0.7.0 was
28
+ published, so it has not reached gem users yet and was never recorded here.)*
29
+ - Expanded the `Kaizo/NestedMethodCalls` README guidance to spell out that the
30
+ *name* is the point, not the assignment — extracting to a local called `result`
31
+ or `tmp` satisfies the rule while missing what it asks for. The offense message
32
+ itself is unchanged.
33
+
3
34
  ## 0.6.0
4
35
 
5
36
  - **Renamed the gem from `rubocop-design` to `kaizo`**, and moved every cop from
data/README.md CHANGED
@@ -129,6 +129,24 @@ Data.define(:width, :height, :depth, :weight) do
129
129
  end
130
130
  ```
131
131
 
132
+ **Operator methods** are exempt too. The arity of `[]=` is fixed by Ruby's syntax
133
+ — an index (or indices) plus the value being assigned — so there is no object to
134
+ extract and no primitive obsession to correct. The same holds for `[]`, `<=>`,
135
+ `+`, `<<`, `==`, and the rest of the operator family:
136
+
137
+ ```ruby
138
+ # not flagged
139
+ def []=(row, column, value)
140
+ @cells[row][column] = value
141
+ end
142
+ ```
143
+
144
+ This covers `def`, `def self.`, and `define_method(:[]=)`. A `define_method` whose
145
+ name is computed at runtime is still checked — the cop cannot know what the name
146
+ resolves to. Note the exemption is for *operator* methods, not ordinary writers:
147
+ `def name=(value)` takes a single argument and was never in danger of tripping the
148
+ limits anyway.
149
+
132
150
  There is intentionally **no autocorrection**: the fix is a design decision (what
133
151
  object should these arguments become?), and that belongs to a human.
134
152
 
@@ -214,13 +232,21 @@ intermediate results want names. Reaching for the right name (or extracting a
214
232
  method) almost always reads better, and is easier to debug, than peeling
215
233
  parentheses apart.
216
234
 
235
+ The point is not the assignment, it is the **name**. A local called `result` or
236
+ `tmp` buys nothing; a name that says what the value *is* turns the step into its
237
+ own documentation.
238
+
217
239
  ```ruby
218
240
  # bad
219
241
  wrap(parse(read(io)))
220
242
 
221
- # good - name the intermediate result
222
- parsed = parse(read(io))
223
- wrap(parsed)
243
+ # bad - named, but the name says nothing
244
+ result = parse(read(io))
245
+ wrap(result)
246
+
247
+ # good - the name documents what the value is
248
+ parsed_config = parse(read(io))
249
+ wrap(parsed_config)
224
250
 
225
251
  # good - a single nested call is fine
226
252
  puts compute(value)
@@ -515,6 +541,56 @@ Kaizo/NextInNonVoidEnumerable:
515
541
  AllowedPatterns: []
516
542
  ```
517
543
 
544
+ ## Plural names for collections
545
+
546
+ `Kaizo/PluralCollectionName` flags a method that hands back an array under a
547
+ singular name. The plural does the documenting for free — `users` tells the
548
+ caller what they are getting; `user` actively misleads them.
549
+
550
+ ```ruby
551
+ # bad
552
+ def user
553
+ [first_match, second_match]
554
+ end
555
+
556
+ # good
557
+ def users
558
+ [first_match, second_match]
559
+ end
560
+ ```
561
+
562
+ Ruby has no return types, so "returns an array" is a heuristic — and this cop
563
+ deliberately errs toward silence. A method is flagged only when **every** value
564
+ it can return is unambiguously an array: an array literal, or a call to a method
565
+ in `ArrayMethods` whose result is an `Array` whatever its receiver. A single
566
+ branch returning something else is enough to leave the method alone:
567
+
568
+ ```ruby
569
+ # good - not confidently a collection, so not flagged
570
+ def user
571
+ return nil if missing?
572
+
573
+ [first_match, second_match]
574
+ end
575
+ ```
576
+
577
+ `select` and `reject` are absent from the default `ArrayMethods` on purpose: on a
578
+ `Hash` they return a `Hash`, and including them would turn this into a
579
+ false-positive mill. A name counts as plural when it ends in `s` or appears in
580
+ `IrregularPlurals`. Predicate (`?`), writer (`=`), and operator methods are
581
+ exempt, as is `initialize`.
582
+
583
+ ```yaml
584
+ Kaizo/PluralCollectionName:
585
+ AllowedMethods: []
586
+ IrregularPlurals:
587
+ - people # plural without a trailing `s`
588
+ - children
589
+ ```
590
+
591
+ As with the other cops there is **no autocorrection** — only the author knows
592
+ the right plural.
593
+
518
594
  ## Development
519
595
 
520
596
  ```bash
data/config/default.yml CHANGED
@@ -7,15 +7,38 @@
7
7
 
8
8
  # The `Kaizo/*` cops check `def`, `def self.`, `define_method`, and
9
9
  # `define_singleton_method`. `*rest`, `**keyword-rest`, and `&block` parameters
10
- # are not counted. The defaults are deliberately strict -- at most one positional
11
- # and one keyword argument -- to apply maximum pressure; loosen them if that is
12
- # too aggressive for your codebase.
10
+ # are not counted. Operator methods (`[]=`, `[]`, `<=>`, ...) and the `initialize`
11
+ # of a `Struct.new`/`Data.define` block are exempt -- their arity is fixed by Ruby
12
+ # syntax or by the value object's members, not by a modeling choice. The defaults
13
+ # are deliberately strict -- at most one positional and one keyword argument -- to
14
+ # apply maximum pressure; loosen them if that is too aggressive for your codebase.
13
15
  Kaizo/KeywordArguments:
14
16
  Description: 'Checks that a method does not declare too many keyword arguments.'
15
17
  Enabled: true
16
18
  VersionAdded: '0.1'
17
19
  Max: 1
18
20
 
21
+ # `Kaizo/PluralCollectionName` flags a method that hands back an array under a
22
+ # singular name. Ruby has no return types, so this is a heuristic, and it errs
23
+ # toward silence: a method is flagged only when every value it can return is
24
+ # unambiguously an array. `ArrayMethods` is the set whose result is an `Array`
25
+ # whatever the receiver -- `select` and `reject` are absent on purpose, since on
26
+ # a `Hash` they return a `Hash`. A name counts as plural when it ends in `s` or
27
+ # appears in `IrregularPlurals`.
28
+ Kaizo/PluralCollectionName:
29
+ Description: 'Checks that a method returning a collection is named in the plural.'
30
+ Enabled: true
31
+ VersionAdded: '0.8'
32
+ AllowedMethods: []
33
+ IrregularPlurals:
34
+ - people
35
+ - children
36
+ - men
37
+ - women
38
+ - data
39
+ - media
40
+ - criteria
41
+
19
42
  Kaizo/PositionalArguments:
20
43
  Description: 'Checks that a method does not declare too many positional arguments.'
21
44
  Enabled: true
@@ -153,6 +176,14 @@ Kaizo/NextInNonVoidEnumerable:
153
176
  # `.rubocop.yml` at your peril.
154
177
  Style/RedundantBegin:
155
178
  Enabled: false
179
+
180
+ # Prefer Ruby 3.1's hash-value shorthand -- `Session.new(table:)` over
181
+ # `Session.new(table: table)` -- everywhere key and value name match. Shipped on
182
+ # by default because kaizo targets modern Ruby; override in your own config if
183
+ # you want the explicit form.
184
+ Style/HashSyntax:
185
+ EnforcedShorthandSyntax: always
186
+
156
187
  # `Kaizo/SpecDescriptionProse` requires RSpec `it`/`context` descriptions to
157
188
  # read as one-behavior prose: no commas, conjunctions (`Conjunctions`), or code
158
189
  # in an `it` description; `context` descriptions carry no code and open with a
data/lib/kaizo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kaizo
2
- VERSION = "0.7.0".freeze
2
+ VERSION = "0.8.0".freeze
3
3
  end
@@ -58,7 +58,7 @@ module RuboCop
58
58
  def check_name(name, location)
59
59
  return unless offending?(name)
60
60
 
61
- add_offense(location, message: format(MSG, name: name))
61
+ add_offense(location, message: format(MSG, name:))
62
62
  end
63
63
 
64
64
  def offending?(name)
@@ -6,30 +6,60 @@ module RuboCop
6
6
  # Walks method definitions written with `def`, `def self.`, `define_method`,
7
7
  # and `define_singleton_method`, and reports when the argument count
8
8
  # produced by the including cop exceeds the configured `Max`. Including cops
9
- # must define a private `arity(arguments)` method and a `KIND` constant. The
10
- # `initialize` of a `Struct.new`/`Data.define` block is exempt.
9
+ # must define a private `arity(arguments)` method and a `KIND` constant.
10
+ # Operator methods (`[]=`, `[]`, `<=>`, ...) and the `initialize` of a
11
+ # `Struct.new`/`Data.define` block are exempt.
11
12
  module ArgumentCounting
12
13
  POSITIONAL_TYPES = %i[arg optarg].freeze
13
14
  KEYWORD_TYPES = %i[kwarg kwoptarg].freeze
14
15
  DEFINE_METHODS = %i[define_method define_singleton_method].freeze
15
16
  STRUCT_OR_DATA = { "Struct" => :new, "Data" => :define }.freeze
17
+
18
+ # Ruby's operator method names, mirroring the list behind RuboCop's
19
+ # `operator_method?` (private upstream, so it cannot be reused). Needed
20
+ # only for the `define_method(:[]=)` form, where the name being defined is
21
+ # a symbol argument rather than a `def` node we could ask directly.
22
+ OPERATOR_METHOD_NAMES = [
23
+ :!, :!=, :"!@", :!~, :%, :&, :*, :**, :+, :+@, :-, :-@, :/, :<, :<<, :<=,
24
+ :<=>, :==, :===, :=~, :>, :>=, :>>, :[], :[]=, :^, :`, :|, :~, :"~@"
25
+ ].freeze
16
26
  MSG = "Method has too many %<kind>s. [%<count>d/%<max>d]".freeze
17
27
 
18
28
  def on_def(node)
19
- return if allowed_initialize?(node)
29
+ return if exempt?(node)
20
30
 
21
31
  check_arity(node)
22
32
  end
23
33
  alias on_defs on_def
24
34
 
25
35
  def on_block(node)
26
- check_arity(node) if DEFINE_METHODS.include?(node.method_name)
36
+ return unless DEFINE_METHODS.include?(node.method_name)
37
+ return if defines_operator?(node)
38
+
39
+ check_arity(node)
27
40
  end
28
41
  alias on_numblock on_block
29
42
  alias on_itblock on_block
30
43
 
31
44
  private
32
45
 
46
+ # A definition whose argument count is not a design choice. An operator
47
+ # method's arity is fixed by Ruby's syntax -- `[]=` takes the indices plus
48
+ # the assigned value, `<=>` takes its right-hand side -- so it cannot be
49
+ # modeled away, and a `Struct`/`Data` `initialize` just mirrors the members
50
+ # the value object was declared with.
51
+ def exempt?(node)
52
+ node.operator_method? || allowed_initialize?(node)
53
+ end
54
+
55
+ # The same exemption for `define_method(:[]=)`, which names the method it
56
+ # defines with a symbol argument. A computed name is still checked -- we
57
+ # cannot know what it resolves to.
58
+ def defines_operator?(node)
59
+ defined_name = node.send_node.first_argument
60
+ defined_name&.sym_type? && OPERATOR_METHOD_NAMES.include?(defined_name.value)
61
+ end
62
+
33
63
  def positional_arity(arguments)
34
64
  arguments.count { |argument| POSITIONAL_TYPES.include?(argument.type) }
35
65
  end
@@ -43,8 +73,8 @@ module RuboCop
43
73
  count = arity(node.arguments)
44
74
  return unless max && count > max
45
75
 
46
- message = format(MSG, kind: self.class::KIND, count: count, max: max)
47
- add_offense(offense_location(node), message: message) { self.max = count }
76
+ message = format(MSG, kind: self.class::KIND, count:, max:)
77
+ add_offense(offense_location(node), message:) { self.max = count }
48
78
  end
49
79
 
50
80
  def offense_location(node)
@@ -68,7 +68,7 @@ module RuboCop
68
68
  count = own_sends(node) { |send| file_utils_call?(send) }.size
69
69
  return if count < 2
70
70
 
71
- add_offense(node.loc.name, message: format(MSG, count: count, scope: scope))
71
+ add_offense(node.loc.name, message: format(MSG, count:, scope:))
72
72
  end
73
73
 
74
74
  # Sends in `namespace`'s own body that match the block -- not its
@@ -5,8 +5,10 @@ module RuboCop
5
5
  #
6
6
  # A call whose arguments are themselves the results of other calls --
7
7
  # `foo(SomeClass.new(another("bar").chain))` -- packs several steps into one
8
- # expression. Naming the intermediate results (or extracting a method) almost
9
- # always reads better and is easier to debug than peeling parentheses apart.
8
+ # expression. Giving the intermediate results descriptive names (or extracting
9
+ # a method) almost always reads better and is easier to debug than peeling
10
+ # parentheses apart. The point is not the assignment but the name: it should
11
+ # say what the value is, so the step documents itself.
10
12
  #
11
13
  # Only nesting through *argument* positions is counted; a receiver chain such
12
14
  # as `user.account.owner.name` is a separate concern. Operator methods
@@ -21,9 +23,9 @@ module RuboCop
21
23
  # # bad
22
24
  # wrap(parse(read(io)))
23
25
  #
24
- # # good - name the intermediate result
25
- # parsed = parse(read(io))
26
- # wrap(parsed)
26
+ # # good - a name that documents what the value is
27
+ # parsed_config = parse(read(io))
28
+ # wrap(parsed_config)
27
29
  #
28
30
  # # good - a single nested call is allowed
29
31
  # puts compute(value)
@@ -48,7 +50,7 @@ module RuboCop
48
50
  depth = nesting_depth(node)
49
51
  return unless max && depth > max
50
52
 
51
- add_offense(node, message: format(MSG, depth: depth, max: max)) do
53
+ add_offense(node, message: format(MSG, depth:, max:)) do
52
54
  self.max = depth
53
55
  end
54
56
  end
@@ -113,11 +113,11 @@ module RuboCop
113
113
  private
114
114
 
115
115
  def flag_block_local_nexts(block_node, method)
116
- message = format(MSG, method: method)
116
+ message = format(MSG, method:)
117
117
 
118
118
  block_node.each_child_node do |child|
119
119
  each_block_local_next(child) do |next_node|
120
- add_offense(next_node.loc.keyword, message: message)
120
+ add_offense(next_node.loc.keyword, message:)
121
121
  end
122
122
  end
123
123
  end
@@ -0,0 +1,111 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Checks that a method returning a collection is named in the plural. A
5
+ # singular name on a method handing back an array (`def user` returning
6
+ # `[first, second]`) misdescribes what the caller gets; the plural does the
7
+ # documenting for free.
8
+ #
9
+ # Ruby has no return types, so "returns an array" is a heuristic, and this
10
+ # cop deliberately errs toward silence. A method is only flagged when
11
+ # *every* value it can return is unambiguously an array: an array literal,
12
+ # or a call to a method that returns an `Array` whatever its receiver
13
+ # (`ArrayMethods`, e.g. `map`, `to_a`, `sort`). One branch returning `nil`
14
+ # is enough to leave the method alone. Methods like `select` and `reject`
15
+ # are absent by design -- on a `Hash` they hand back a `Hash`.
16
+ #
17
+ # A name counts as plural when it ends in `s` or appears in
18
+ # `IrregularPlurals`. Predicate (`?`), writer (`=`), and operator methods
19
+ # are exempt, as is `initialize`, and `AllowedMethods` exempts names
20
+ # outright. There is no autocorrection: renaming a method is a design
21
+ # decision, and only its author knows the right plural.
22
+ #
23
+ # @example
24
+ # # bad
25
+ # def user
26
+ # [first_match, second_match]
27
+ # end
28
+ #
29
+ # # good
30
+ # def users
31
+ # [first_match, second_match]
32
+ # end
33
+ #
34
+ # @example
35
+ # # good - not confidently an array, so not flagged
36
+ # def user
37
+ # return nil if missing?
38
+ #
39
+ # [first_match, second_match]
40
+ # end
41
+ #
42
+ class PluralCollectionName < Base
43
+ include AllowedMethods
44
+
45
+ MSG = "Name a method that returns a collection in the plural. " \
46
+ "`%<name>s` returns an array.".freeze
47
+
48
+ # Methods whose result is an `Array` regardless of the receiver. Kept
49
+ # deliberately short: anything whose return type follows its receiver
50
+ # (`select` on a `Hash`) would turn this cop into a false-positive mill.
51
+ ARRAY_METHODS = %i[
52
+ map flat_map collect collect_concat to_a entries sort sort_by zip
53
+ ].freeze
54
+
55
+ def on_def(node)
56
+ return if exempt?(node)
57
+ return unless returns_array?(node.body)
58
+
59
+ add_offense(node.loc.name, message: format(MSG, name: node.method_name))
60
+ end
61
+ alias on_defs on_def
62
+
63
+ private
64
+
65
+ def exempt?(node)
66
+ return true if node.predicate_method? || node.assignment_method?
67
+ return true if node.operator_method? || node.method?(:initialize)
68
+
69
+ plural?(node.method_name.to_s) || allowed_method?(node.method_name)
70
+ end
71
+
72
+ def plural?(name)
73
+ name.end_with?("s") || irregular_plurals.include?(name)
74
+ end
75
+
76
+ def irregular_plurals
77
+ Array(cop_config["IrregularPlurals"])
78
+ end
79
+
80
+ # Every value the method can hand back must be an array before it is
81
+ # worth flagging, so that a single `return nil` keeps the cop quiet.
82
+ def returns_array?(body)
83
+ return false unless body
84
+
85
+ results = [final_expression(body), *explicit_returns(body)]
86
+ results.all? { |result| array_result?(result) }
87
+ end
88
+
89
+ def final_expression(body)
90
+ body.begin_type? ? body.children.last : body
91
+ end
92
+
93
+ # The value of each `return`, with a bare `return` contributing `nil` --
94
+ # which is exactly what should stop the method being flagged.
95
+ def explicit_returns(body)
96
+ body.each_descendant(:return).map { |node| node.children.first }
97
+ end
98
+
99
+ # A block-bearing call (`rows.map { ... }`) is a block node wrapping the
100
+ # send, so the method name lives one level down.
101
+ def array_result?(node)
102
+ return false unless node
103
+ return true if node.array_type?
104
+
105
+ call = node.any_block_type? ? node.send_node : node
106
+ call.call_type? && ARRAY_METHODS.include?(call.method_name)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -78,7 +78,7 @@ module RuboCop
78
78
  message = violation(node.method_name, text)
79
79
  return unless message
80
80
 
81
- add_offense(node.first_argument, message: message)
81
+ add_offense(node.first_argument, message:)
82
82
  end
83
83
 
84
84
  private
@@ -95,7 +95,7 @@ module RuboCop
95
95
  return COMMA_MSG if text.include?(",")
96
96
 
97
97
  word = forbidden(text, conjunctions)
98
- return format(CONJUNCTION_MSG, word: word) if word
98
+ return format(CONJUNCTION_MSG, word:) if word
99
99
 
100
100
  CODE_MSG if code?(text)
101
101
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "kaizo/argument_counting"
2
2
  require_relative "kaizo/keyword_arguments"
3
+ require_relative "kaizo/plural_collection_name"
3
4
  require_relative "kaizo/positional_arguments"
4
5
  require_relative "kaizo/total_arguments"
5
6
  require_relative "kaizo/agent_noun_class_name"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaizo
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
  - David Gillis
@@ -62,6 +62,7 @@ files:
62
62
  - lib/rubocop/cop/kaizo/keyword_arguments.rb
63
63
  - lib/rubocop/cop/kaizo/nested_method_calls.rb
64
64
  - lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb
65
+ - lib/rubocop/cop/kaizo/plural_collection_name.rb
65
66
  - lib/rubocop/cop/kaizo/positional_arguments.rb
66
67
  - lib/rubocop/cop/kaizo/prefer_pathname.rb
67
68
  - lib/rubocop/cop/kaizo/spec_comment.rb
@@ -90,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
91
  - !ruby/object:Gem::Version
91
92
  version: '0'
92
93
  requirements: []
93
- rubygems_version: 4.0.16
94
+ rubygems_version: 4.0.17
94
95
  specification_version: 4
95
96
  summary: A strict, punishing set of RuboCop design cops for AI-agent-authored Ruby.
96
97
  test_files: []