kaizo 0.7.0 → 0.9.2

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.
data/config/default.yml CHANGED
@@ -7,26 +7,84 @@
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.
15
+ # Exempt individual methods by name (`AllowedMethods`, e.g. `initialize`) or by
16
+ # regexp (`AllowedPatterns`).
17
+ #
18
+ # The keyword-counting cops (`KeywordArguments`, `TotalArguments`) skip `spec/`
19
+ # and `test/` trees: wide keyword interfaces are the testing idiom there
20
+ # (FactoryBot-style `create`/`build` helpers), and the total bound would
21
+ # re-police the keywords the exemption frees. `PositionalArguments` runs
22
+ # everywhere -- positional arguments communicate nothing unless they are solo.
13
23
  Kaizo/KeywordArguments:
14
24
  Description: 'Checks that a method does not declare too many keyword arguments.'
15
25
  Enabled: true
16
26
  VersionAdded: '0.1'
27
+ VersionChanged: '0.9.1'
17
28
  Max: 1
29
+ AllowedMethods: []
30
+ AllowedPatterns: []
31
+ Exclude:
32
+ - '**/spec/**/*'
33
+ - '**/test/**/*'
34
+
35
+ # `Kaizo/PluralCollectionName` flags a method that hands back an array under a
36
+ # singular name. Ruby has no return types, so this is a heuristic, and it errs
37
+ # toward silence: a method is flagged only when every value it can return is
38
+ # unambiguously an array. `ArrayMethods` is the set whose result is an `Array`
39
+ # whatever the receiver -- `select` and `reject` are absent on purpose, since on
40
+ # a `Hash` they return a `Hash`; extend it (with `inherit_mode: merge`) to
41
+ # cover your own collection-returning helpers. A name counts as plural when it
42
+ # ends in `s` or appears in `IrregularPlurals`.
43
+ Kaizo/PluralCollectionName:
44
+ Description: 'Checks that a method returning a collection is named in the plural.'
45
+ Enabled: true
46
+ VersionAdded: '0.8'
47
+ VersionChanged: '0.9'
48
+ AllowedMethods: []
49
+ ArrayMethods:
50
+ - map
51
+ - flat_map
52
+ - collect
53
+ - collect_concat
54
+ - to_a
55
+ - entries
56
+ - sort
57
+ - sort_by
58
+ - zip
59
+ IrregularPlurals:
60
+ - people
61
+ - children
62
+ - men
63
+ - women
64
+ - data
65
+ - media
66
+ - criteria
18
67
 
19
68
  Kaizo/PositionalArguments:
20
69
  Description: 'Checks that a method does not declare too many positional arguments.'
21
70
  Enabled: true
22
71
  VersionAdded: '0.1'
72
+ VersionChanged: '0.9'
23
73
  Max: 1
74
+ AllowedMethods: []
75
+ AllowedPatterns: []
24
76
 
25
77
  Kaizo/TotalArguments:
26
78
  Description: 'Checks that a method does not declare too many arguments in total.'
27
79
  Enabled: true
28
80
  VersionAdded: '0.1'
81
+ VersionChanged: '0.9.1'
29
82
  Max: 2
83
+ AllowedMethods: []
84
+ AllowedPatterns: []
85
+ Exclude:
86
+ - '**/spec/**/*'
87
+ - '**/test/**/*'
30
88
 
31
89
  # `Kaizo/AgentNounClassName` flags `class` definitions and
32
90
  # `Struct.new`/`Data.define`/`Class.new` assignments whose name ends in `er`/`or`
@@ -114,12 +172,18 @@ Kaizo/NestedMethodCalls:
114
172
  # example. Magic comments, `# rubocop:` directives, and shebangs are exempt;
115
173
  # add more exemptions with `AllowedPatterns`. Broaden `Include` (e.g. add
116
174
  # `**/spec/**/*` or `**/*_test.rb`) to cover support files or Minitest.
175
+ # `spec/helpers` and `spec/support` hold infrastructure, not specs, so they
176
+ # are excluded by default; override `Exclude` to change that.
117
177
  Kaizo/SpecComment:
118
178
  Description: 'Avoid comments in spec files; express the intent as well-structured specs.'
119
179
  Enabled: true
120
180
  VersionAdded: '0.5'
181
+ VersionChanged: '0.9'
121
182
  Include:
122
183
  - '**/*_spec.rb'
184
+ Exclude:
185
+ - '**/spec/helpers/**/*'
186
+ - '**/spec/support/**/*'
123
187
  AllowedPatterns: []
124
188
 
125
189
  # `Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
@@ -153,17 +217,31 @@ Kaizo/NextInNonVoidEnumerable:
153
217
  # `.rubocop.yml` at your peril.
154
218
  Style/RedundantBegin:
155
219
  Enabled: false
220
+
221
+ # Prefer Ruby 3.1's hash-value shorthand -- `Session.new(table:)` over
222
+ # `Session.new(table: table)` -- everywhere key and value name match. Shipped on
223
+ # by default because kaizo targets modern Ruby; override in your own config if
224
+ # you want the explicit form.
225
+ Style/HashSyntax:
226
+ EnforcedShorthandSyntax: always
227
+
156
228
  # `Kaizo/SpecDescriptionProse` requires RSpec `it`/`context` descriptions to
157
- # read as one-behavior prose: no commas, conjunctions (`Conjunctions`), or code
158
- # in an `it` description; `context` descriptions carry no code and open with a
159
- # `ContextPrefixes` word. `describe` strings are exempt. No autocorrection.
229
+ # read as one-behavior prose: no commas, none of the `ForbiddenWords`
230
+ # (conjunctions and conditionals whose presence signals a hidden split), and no
231
+ # code in an `it` description; `context` descriptions carry no code and must
232
+ # open with one of the `RequiredContextPrefixes`. Error class names
233
+ # (`Foo::Error`) read as prose, since the error is part of the specified
234
+ # behavior. Exempt whole descriptions with `AllowedPatterns`. `describe`
235
+ # strings are exempt. No autocorrection.
160
236
  Kaizo/SpecDescriptionProse:
161
237
  Description: 'Require RSpec it/context descriptions to read as one-behavior prose.'
162
238
  Enabled: true
163
239
  VersionAdded: '0.6'
240
+ VersionChanged: '0.9'
164
241
  Include:
165
242
  - '**/*_spec.rb'
166
- Conjunctions:
243
+ AllowedPatterns: []
244
+ ForbiddenWords:
167
245
  - and
168
246
  - but
169
247
  - or
@@ -179,12 +257,28 @@ Kaizo/SpecDescriptionProse:
179
257
  - because
180
258
  - although
181
259
  - though
182
- ContextPrefixes:
260
+ RequiredContextPrefixes:
183
261
  - when
184
262
  - with
185
263
  - without
186
264
  - after
187
265
 
266
+ # `Kaizo/SpecSubject` requires the unit under test to be declared with
267
+ # `subject`, not `let`. A `let`/`let!` is flagged when its block confidently
268
+ # builds the class under test: a `.new` on `described_class`, on the constant
269
+ # an enclosing `describe`/`context` names (full or short name), or on a
270
+ # constant matching the spec's file name. Deliberate second instances (an
271
+ # `other` in an equality spec) are exempted with `AllowedMethods` /
272
+ # `AllowedPatterns`, matched against the `let` name. No autocorrection.
273
+ Kaizo/SpecSubject:
274
+ Description: 'Declare the unit under test with subject, not let.'
275
+ Enabled: true
276
+ VersionAdded: '0.9'
277
+ Include:
278
+ - '**/*_spec.rb'
279
+ AllowedMethods: []
280
+ AllowedPatterns: []
281
+
188
282
  # `Kaizo/FileUtilsInclusion` requires `FileUtils` to be mixed in with `include`
189
283
  # or `extend` once it is used more than once in a class or module, instead of
190
284
  # repeating the `FileUtils.` receiver. The namespace is reported once. A single
@@ -195,6 +289,16 @@ Kaizo/FileUtilsInclusion:
195
289
  Enabled: true
196
290
  VersionAdded: '0.6'
197
291
 
292
+ # `Kaizo/TempfileCreate` requires block-form `Tempfile.create`, the only
293
+ # temp-file API that closes and removes the file deterministically.
294
+ # `Tempfile.new`/`Tempfile.open` rely on a GC finalizer that may run late or
295
+ # never; blockless `Tempfile.create` returns a plain `File` that is never
296
+ # removed automatically. No autocorrection.
297
+ Kaizo/TempfileCreate:
298
+ Description: 'Use block-form Tempfile.create; Tempfile.new/open clean up unpredictably.'
299
+ Enabled: true
300
+ VersionAdded: '0.9'
301
+
198
302
  # `Kaizo/PreferPathname` flags calls to `File` class methods that have a
199
303
  # `Pathname` instance-method equivalent (`File.read`, `File.exist?`, `File.join`,
200
304
  # ...), preferring `Pathname`. Runs on `**/*.rb`; `exe/**/*` and `bin/**/*` are
data/lib/kaizo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kaizo
2
- VERSION = "0.7.0".freeze
2
+ VERSION = "0.9.2".freeze
3
3
  end
@@ -16,6 +16,29 @@ module RuboCop
16
16
  # assignments are both checked. There is no autocorrection: renaming a
17
17
  # class is a design decision.
18
18
  #
19
+ # == Configuration
20
+ #
21
+ # [+AllowedSuffixes+] Suffixes exempt from the `er`/`or` rule, matched
22
+ # against the end of the name (+Controller+ clears
23
+ # +UsersController+ too). Default: a broad list of
24
+ # legitimate `-er`/`-or` domain nouns and framework
25
+ # terms (+Adapter+, +Controller+, +Error+, +User+,
26
+ # ...) -- see +config/default.yml+ for all of them.
27
+ # [+ForbiddenSuffixes+] Suffixes always flagged, even when matched by
28
+ # +AllowedSuffixes+ -- which is how a default
29
+ # exemption is dropped. Default: +Service+, +Util+,
30
+ # +Utils+.
31
+ #
32
+ # Extend either list without restating it via RuboCop's
33
+ # <tt>inherit_mode: merge</tt>:
34
+ #
35
+ # Kaizo/AgentNounClassName:
36
+ # inherit_mode:
37
+ # merge:
38
+ # - AllowedSuffixes
39
+ # AllowedSuffixes:
40
+ # - Voucher # PaymentVoucher now passes
41
+ #
19
42
  # @example
20
43
  # # bad
21
44
  # class PaymentProcessor
@@ -58,7 +81,7 @@ module RuboCop
58
81
  def check_name(name, location)
59
82
  return unless offending?(name)
60
83
 
61
- add_offense(location, message: format(MSG, name: name))
84
+ add_offense(location, message: format(MSG, name:))
62
85
  end
63
86
 
64
87
  def offending?(name)
@@ -6,30 +6,70 @@ 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
13
+ include AllowedMethods
14
+ include AllowedPattern
15
+
12
16
  POSITIONAL_TYPES = %i[arg optarg].freeze
13
17
  KEYWORD_TYPES = %i[kwarg kwoptarg].freeze
14
18
  DEFINE_METHODS = %i[define_method define_singleton_method].freeze
15
19
  STRUCT_OR_DATA = { "Struct" => :new, "Data" => :define }.freeze
20
+
21
+ # Ruby's operator method names, mirroring the list behind RuboCop's
22
+ # `operator_method?` (private upstream, so it cannot be reused). Needed
23
+ # only for the `define_method(:[]=)` form, where the name being defined is
24
+ # a symbol argument rather than a `def` node we could ask directly.
25
+ OPERATOR_METHOD_NAMES = [
26
+ :!, :!=, :"!@", :!~, :%, :&, :*, :**, :+, :+@, :-, :-@, :/, :<, :<<, :<=,
27
+ :<=>, :==, :===, :=~, :>, :>=, :>>, :[], :[]=, :^, :`, :|, :~, :"~@"
28
+ ].freeze
16
29
  MSG = "Method has too many %<kind>s. [%<count>d/%<max>d]".freeze
17
30
 
18
31
  def on_def(node)
19
- return if allowed_initialize?(node)
32
+ return if exempt?(node)
20
33
 
21
34
  check_arity(node)
22
35
  end
23
36
  alias on_defs on_def
24
37
 
25
38
  def on_block(node)
26
- check_arity(node) if DEFINE_METHODS.include?(node.method_name)
39
+ return unless DEFINE_METHODS.include?(node.method_name)
40
+ return if exempt_defined_name?(node)
41
+
42
+ check_arity(node)
27
43
  end
28
44
  alias on_numblock on_block
29
45
  alias on_itblock on_block
30
46
 
31
47
  private
32
48
 
49
+ # A definition whose argument count is not a design choice. An operator
50
+ # method's arity is fixed by Ruby's syntax -- `[]=` takes the indices plus
51
+ # the assigned value, `<=>` takes its right-hand side -- so it cannot be
52
+ # modeled away, and a `Struct`/`Data` `initialize` just mirrors the members
53
+ # the value object was declared with.
54
+ def exempt?(node)
55
+ node.operator_method? || allowed_initialize?(node) || allowed_name?(node.method_name)
56
+ end
57
+
58
+ # The same exemptions for `define_method(:[]=)`, which names the method it
59
+ # defines with a symbol (or string) argument. A computed name is still
60
+ # checked -- we cannot know what it resolves to.
61
+ def exempt_defined_name?(node)
62
+ defined_name = node.send_node.first_argument
63
+ return false unless defined_name&.type?(:sym, :str)
64
+
65
+ name = defined_name.value.to_sym
66
+ OPERATOR_METHOD_NAMES.include?(name) || allowed_name?(name)
67
+ end
68
+
69
+ def allowed_name?(name)
70
+ allowed_method?(name) || matches_allowed_pattern?(name.to_s)
71
+ end
72
+
33
73
  def positional_arity(arguments)
34
74
  arguments.count { |argument| POSITIONAL_TYPES.include?(argument.type) }
35
75
  end
@@ -43,8 +83,8 @@ module RuboCop
43
83
  count = arity(node.arguments)
44
84
  return unless max && count > max
45
85
 
46
- message = format(MSG, kind: self.class::KIND, count: count, max: max)
47
- add_offense(offense_location(node), message: message) { self.max = count }
86
+ message = format(MSG, kind: self.class::KIND, count:, max:)
87
+ add_offense(offense_location(node), message:) { self.max = count }
48
88
  end
49
89
 
50
90
  def offense_location(node)
@@ -15,6 +15,15 @@ module RuboCop
15
15
  # Modifier `rescue` expressions (`foo rescue nil`) and endless method
16
16
  # definitions are not flagged.
17
17
  #
18
+ # == Configuration
19
+ #
20
+ # No cop-specific options; the standard per-cop settings (+Enabled+,
21
+ # +Severity+, +AutoCorrect+, +Include+/+Exclude+) apply. Note that
22
+ # loading the kaizo plugin disables `Style/RedundantBegin`, this cop's
23
+ # exact inverse. To opt out of explicit begins, disable this cop;
24
+ # re-enable `Style/RedundantBegin` only alongside that, since with both
25
+ # enabled each flags the form the other mandates.
26
+ #
18
27
  # @safety
19
28
  # Autocorrection is skipped when the body does not sit on its own lines
20
29
  # between `def` and `end` (a single-line definition, for example), and
@@ -19,6 +19,11 @@ module RuboCop
19
19
  # There is no autocorrection: whether to `include` or `extend`, and where
20
20
  # the mixin belongs, is a judgment call for a human.
21
21
  #
22
+ # == Configuration
23
+ #
24
+ # No cop-specific options; the standard per-cop settings (+Enabled+,
25
+ # +Severity+, +Include+/+Exclude+) apply.
26
+ #
22
27
  # @example
23
28
  # # bad - the `FileUtils.` receiver is repeated
24
29
  # class Backup
@@ -68,7 +73,7 @@ module RuboCop
68
73
  count = own_sends(node) { |send| file_utils_call?(send) }.size
69
74
  return if count < 2
70
75
 
71
- add_offense(node.loc.name, message: format(MSG, count: count, scope: scope))
76
+ add_offense(node.loc.name, message: format(MSG, count:, scope:))
72
77
  end
73
78
 
74
79
  # Sends in `namespace`'s own body that match the block -- not its
@@ -8,6 +8,23 @@ module RuboCop
8
8
  # (`kwarg`) and optional (`kwoptarg`) parameters are counted; `**rest` is
9
9
  # not.
10
10
  #
11
+ # == Configuration
12
+ #
13
+ # [+Max+] Most keyword arguments a method may declare. Default: +1+.
14
+ # [+AllowedMethods+] Method names exempt from the limit. Default: none.
15
+ # [+AllowedPatterns+] Regexps matched against the method name; a match is
16
+ # exempt. Default: none.
17
+ # [+Exclude+] Paths the cop skips. Default: <tt>**/spec/**/*</tt> and
18
+ # <tt>**/test/**/*</tt> -- wide keyword interfaces are the
19
+ # testing idiom (FactoryBot-style builders), so keyword
20
+ # counts are not policed there. Set it to <tt>[]</tt> to
21
+ # police tests too.
22
+ #
23
+ # Kaizo/KeywordArguments:
24
+ # Max: 2
25
+ # AllowedMethods:
26
+ # - initialize
27
+ #
11
28
  # @example Max: 2
12
29
  # # bad
13
30
  # def calculate_volume(width:, length:, height:)
@@ -17,6 +34,11 @@ module RuboCop
17
34
  # def calculate_volume(shape)
18
35
  # end
19
36
  #
37
+ # @example AllowedMethods: ['initialize'] (default: [])
38
+ # # good - exempt by name
39
+ # def initialize(host:, port:, ssl: true)
40
+ # end
41
+ #
20
42
  class KeywordArguments < Base
21
43
  include ArgumentCounting
22
44
 
@@ -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
@@ -14,6 +16,18 @@ module RuboCop
14
16
  # Depth is measured from each outermost call and reported once. There is no
15
17
  # autocorrection: choosing the intermediate name is a design decision.
16
18
  #
19
+ # == Configuration
20
+ #
21
+ # [+Max+] Deepest allowed nesting of calls in argument positions; +1+
22
+ # permits one nested call. Default: +1+.
23
+ # [+AllowedMethods+] Calls to these methods neither count toward depth
24
+ # nor get flagged. Default: none.
25
+ #
26
+ # Kaizo/NestedMethodCalls:
27
+ # Max: 2
28
+ # AllowedMethods:
29
+ # - expect # don't count RSpec's expect(...) wrapper
30
+ #
17
31
  # @example Max: 1 (default)
18
32
  # # bad
19
33
  # foo(SomeClass.new(another("bar").chain))
@@ -21,9 +35,9 @@ module RuboCop
21
35
  # # bad
22
36
  # wrap(parse(read(io)))
23
37
  #
24
- # # good - name the intermediate result
25
- # parsed = parse(read(io))
26
- # wrap(parsed)
38
+ # # good - a name that documents what the value is
39
+ # parsed_config = parse(read(io))
40
+ # wrap(parsed_config)
27
41
  #
28
42
  # # good - a single nested call is allowed
29
43
  # puts compute(value)
@@ -48,7 +62,7 @@ module RuboCop
48
62
  depth = nesting_depth(node)
49
63
  return unless max && depth > max
50
64
 
51
- add_offense(node, message: format(MSG, depth: depth, max: max)) do
65
+ add_offense(node, message: format(MSG, depth:, max:)) do
52
66
  self.max = depth
53
67
  end
54
68
  end
@@ -21,6 +21,17 @@ module RuboCop
21
21
  # clause might become a ternary, a `select`/`reject`, a `filter_map`, or a
22
22
  # restructured block.
23
23
  #
24
+ # == Configuration
25
+ #
26
+ # [+AllowedMethods+] Enumerable methods whose blocks may use `next`.
27
+ # Default: none.
28
+ # [+AllowedPatterns+] Regexps matched against the enumerable method's
29
+ # name; a match is exempt. Default: none.
30
+ #
31
+ # Kaizo/NextInNonVoidEnumerable:
32
+ # AllowedMethods:
33
+ # - reduce # allow `next acc` guards in reduce/inject
34
+ #
24
35
  # @example
25
36
  # # bad
26
37
  # array.map do |item|
@@ -113,11 +124,11 @@ module RuboCop
113
124
  private
114
125
 
115
126
  def flag_block_local_nexts(block_node, method)
116
- message = format(MSG, method: method)
127
+ message = format(MSG, method:)
117
128
 
118
129
  block_node.each_child_node do |child|
119
130
  each_block_local_next(child) do |next_node|
120
- add_offense(next_node.loc.keyword, message: message)
131
+ add_offense(next_node.loc.keyword, message:)
121
132
  end
122
133
  end
123
134
  end
@@ -0,0 +1,139 @@
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
+ # == Configuration
24
+ #
25
+ # [+ArrayMethods+] Calls treated as returning an +Array+ whatever their
26
+ # receiver. Default: +map+, +flat_map+, +collect+,
27
+ # +collect_concat+, +to_a+, +entries+, +sort+, +sort_by+,
28
+ # +zip+.
29
+ # [+IrregularPlurals+] Names accepted as plural despite not ending in
30
+ # +s+. Default: +people+, +children+, +men+,
31
+ # +women+, +data+, +media+, +criteria+.
32
+ # [+AllowedMethods+] Method names never flagged. Default: none.
33
+ #
34
+ # Extend a list without restating its default via RuboCop's
35
+ # <tt>inherit_mode: merge</tt>:
36
+ #
37
+ # Kaizo/PluralCollectionName:
38
+ # inherit_mode:
39
+ # merge:
40
+ # - ArrayMethods
41
+ # - IrregularPlurals
42
+ # ArrayMethods:
43
+ # - fetch_all
44
+ # IrregularPlurals:
45
+ # - alumni
46
+ #
47
+ # @example
48
+ # # bad
49
+ # def user
50
+ # [first_match, second_match]
51
+ # end
52
+ #
53
+ # # good
54
+ # def users
55
+ # [first_match, second_match]
56
+ # end
57
+ #
58
+ # @example
59
+ # # good - not confidently an array, so not flagged
60
+ # def user
61
+ # return nil if missing?
62
+ #
63
+ # [first_match, second_match]
64
+ # end
65
+ #
66
+ class PluralCollectionName < Base
67
+ include AllowedMethods
68
+
69
+ MSG = "Name a method that returns a collection in the plural. " \
70
+ "`%<name>s` returns an array.".freeze
71
+
72
+ # Methods whose result is an `Array` regardless of the receiver. Kept
73
+ # deliberately short: anything whose return type follows its receiver
74
+ # (`select` on a `Hash`) would turn this cop into a false-positive mill.
75
+ DEFAULT_ARRAY_METHODS = %i[
76
+ map flat_map collect collect_concat to_a entries sort sort_by zip
77
+ ].freeze
78
+
79
+ def on_def(node)
80
+ return if exempt?(node)
81
+ return unless returns_array?(node.body)
82
+
83
+ add_offense(node.loc.name, message: format(MSG, name: node.method_name))
84
+ end
85
+ alias on_defs on_def
86
+
87
+ private
88
+
89
+ def exempt?(node)
90
+ return true if node.predicate_method? || node.assignment_method?
91
+ return true if node.operator_method? || node.method?(:initialize)
92
+
93
+ plural?(node.method_name.to_s) || allowed_method?(node.method_name)
94
+ end
95
+
96
+ def plural?(name)
97
+ name.end_with?("s") || irregular_plurals.include?(name)
98
+ end
99
+
100
+ def irregular_plurals
101
+ Array(cop_config["IrregularPlurals"])
102
+ end
103
+
104
+ # Every value the method can hand back must be an array before it is
105
+ # worth flagging, so that a single `return nil` keeps the cop quiet.
106
+ def returns_array?(body)
107
+ return false unless body
108
+
109
+ results = [final_expression(body), *explicit_returns(body)]
110
+ results.all? { |result| array_result?(result) }
111
+ end
112
+
113
+ def final_expression(body)
114
+ body.begin_type? ? body.children.last : body
115
+ end
116
+
117
+ # The value of each `return`, with a bare `return` contributing `nil` --
118
+ # which is exactly what should stop the method being flagged.
119
+ def explicit_returns(body)
120
+ body.each_descendant(:return).map { |node| node.children.first }
121
+ end
122
+
123
+ # A block-bearing call (`rows.map { ... }`) is a block node wrapping the
124
+ # send, so the method name lives one level down.
125
+ def array_result?(node)
126
+ return false unless node
127
+ return true if node.array_type?
128
+
129
+ call = node.any_block_type? ? node.send_node : node
130
+ call.call_type? && array_methods.include?(call.method_name)
131
+ end
132
+
133
+ def array_methods
134
+ @array_methods ||= Array(cop_config.fetch("ArrayMethods", DEFAULT_ARRAY_METHODS)).map(&:to_sym)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -8,6 +8,22 @@ module RuboCop
8
8
  # and optional (`optarg`) parameters are counted; `*rest` and `&block` are
9
9
  # not.
10
10
  #
11
+ # Unlike the keyword-counting cops, this one runs everywhere, spec and
12
+ # test trees included: positional arguments communicate nothing unless
13
+ # they are solo, in test code as much as anywhere else.
14
+ #
15
+ # == Configuration
16
+ #
17
+ # [+Max+] Most positional arguments a method may declare. Default: +1+.
18
+ # [+AllowedMethods+] Method names exempt from the limit. Default: none.
19
+ # [+AllowedPatterns+] Regexps matched against the method name; a match is
20
+ # exempt. Default: none.
21
+ #
22
+ # Kaizo/PositionalArguments:
23
+ # Max: 0 # force every argument to be a keyword
24
+ # AllowedMethods:
25
+ # - initialize
26
+ #
11
27
  # @example Max: 2
12
28
  # # bad
13
29
  # def move(x, y, z)
@@ -15,6 +15,19 @@ module RuboCop
15
15
  # There is no autocorrection: rewriting `File.read(path)` as
16
16
  # `Pathname(path).read` changes the receiver and is left to a human.
17
17
  #
18
+ # == Configuration
19
+ #
20
+ # [+Include+] Files the cop runs on. Default: <tt>**/*.rb</tt>.
21
+ # [+Exclude+] Files the cop skips even when included. Default:
22
+ # <tt>exe/**/*</tt> and <tt>bin/**/*</tt>.
23
+ #
24
+ # Kaizo/PreferPathname:
25
+ # inherit_mode:
26
+ # merge:
27
+ # - Exclude
28
+ # Exclude:
29
+ # - 'db/**/*' # add your own
30
+ #
18
31
  # @example
19
32
  # # bad
20
33
  # File.read(path)