kaizo 0.8.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
@@ -12,24 +12,50 @@
12
12
  # syntax or by the value object's members, not by a modeling choice. The defaults
13
13
  # are deliberately strict -- at most one positional and one keyword argument -- to
14
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.
15
23
  Kaizo/KeywordArguments:
16
24
  Description: 'Checks that a method does not declare too many keyword arguments.'
17
25
  Enabled: true
18
26
  VersionAdded: '0.1'
27
+ VersionChanged: '0.9.1'
19
28
  Max: 1
29
+ AllowedMethods: []
30
+ AllowedPatterns: []
31
+ Exclude:
32
+ - '**/spec/**/*'
33
+ - '**/test/**/*'
20
34
 
21
35
  # `Kaizo/PluralCollectionName` flags a method that hands back an array under a
22
36
  # singular name. Ruby has no return types, so this is a heuristic, and it errs
23
37
  # toward silence: a method is flagged only when every value it can return is
24
38
  # unambiguously an array. `ArrayMethods` is the set whose result is an `Array`
25
39
  # 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`.
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`.
28
43
  Kaizo/PluralCollectionName:
29
44
  Description: 'Checks that a method returning a collection is named in the plural.'
30
45
  Enabled: true
31
46
  VersionAdded: '0.8'
47
+ VersionChanged: '0.9'
32
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
33
59
  IrregularPlurals:
34
60
  - people
35
61
  - children
@@ -43,13 +69,22 @@ Kaizo/PositionalArguments:
43
69
  Description: 'Checks that a method does not declare too many positional arguments.'
44
70
  Enabled: true
45
71
  VersionAdded: '0.1'
72
+ VersionChanged: '0.9'
46
73
  Max: 1
74
+ AllowedMethods: []
75
+ AllowedPatterns: []
47
76
 
48
77
  Kaizo/TotalArguments:
49
78
  Description: 'Checks that a method does not declare too many arguments in total.'
50
79
  Enabled: true
51
80
  VersionAdded: '0.1'
81
+ VersionChanged: '0.9.1'
52
82
  Max: 2
83
+ AllowedMethods: []
84
+ AllowedPatterns: []
85
+ Exclude:
86
+ - '**/spec/**/*'
87
+ - '**/test/**/*'
53
88
 
54
89
  # `Kaizo/AgentNounClassName` flags `class` definitions and
55
90
  # `Struct.new`/`Data.define`/`Class.new` assignments whose name ends in `er`/`or`
@@ -137,12 +172,18 @@ Kaizo/NestedMethodCalls:
137
172
  # example. Magic comments, `# rubocop:` directives, and shebangs are exempt;
138
173
  # add more exemptions with `AllowedPatterns`. Broaden `Include` (e.g. add
139
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.
140
177
  Kaizo/SpecComment:
141
178
  Description: 'Avoid comments in spec files; express the intent as well-structured specs.'
142
179
  Enabled: true
143
180
  VersionAdded: '0.5'
181
+ VersionChanged: '0.9'
144
182
  Include:
145
183
  - '**/*_spec.rb'
184
+ Exclude:
185
+ - '**/spec/helpers/**/*'
186
+ - '**/spec/support/**/*'
146
187
  AllowedPatterns: []
147
188
 
148
189
  # `Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
@@ -185,16 +226,22 @@ Style/HashSyntax:
185
226
  EnforcedShorthandSyntax: always
186
227
 
187
228
  # `Kaizo/SpecDescriptionProse` requires RSpec `it`/`context` descriptions to
188
- # read as one-behavior prose: no commas, conjunctions (`Conjunctions`), or code
189
- # in an `it` description; `context` descriptions carry no code and open with a
190
- # `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.
191
236
  Kaizo/SpecDescriptionProse:
192
237
  Description: 'Require RSpec it/context descriptions to read as one-behavior prose.'
193
238
  Enabled: true
194
239
  VersionAdded: '0.6'
240
+ VersionChanged: '0.9'
195
241
  Include:
196
242
  - '**/*_spec.rb'
197
- Conjunctions:
243
+ AllowedPatterns: []
244
+ ForbiddenWords:
198
245
  - and
199
246
  - but
200
247
  - or
@@ -210,12 +257,28 @@ Kaizo/SpecDescriptionProse:
210
257
  - because
211
258
  - although
212
259
  - though
213
- ContextPrefixes:
260
+ RequiredContextPrefixes:
214
261
  - when
215
262
  - with
216
263
  - without
217
264
  - after
218
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
+
219
282
  # `Kaizo/FileUtilsInclusion` requires `FileUtils` to be mixed in with `include`
220
283
  # or `extend` once it is used more than once in a class or module, instead of
221
284
  # repeating the `FileUtils.` receiver. The namespace is reported once. A single
@@ -226,6 +289,16 @@ Kaizo/FileUtilsInclusion:
226
289
  Enabled: true
227
290
  VersionAdded: '0.6'
228
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
+
229
302
  # `Kaizo/PreferPathname` flags calls to `File` class methods that have a
230
303
  # `Pathname` instance-method equivalent (`File.read`, `File.exist?`, `File.join`,
231
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.8.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
@@ -10,6 +10,9 @@ module RuboCop
10
10
  # Operator methods (`[]=`, `[]`, `<=>`, ...) and the `initialize` of a
11
11
  # `Struct.new`/`Data.define` block are exempt.
12
12
  module ArgumentCounting
13
+ include AllowedMethods
14
+ include AllowedPattern
15
+
13
16
  POSITIONAL_TYPES = %i[arg optarg].freeze
14
17
  KEYWORD_TYPES = %i[kwarg kwoptarg].freeze
15
18
  DEFINE_METHODS = %i[define_method define_singleton_method].freeze
@@ -34,7 +37,7 @@ module RuboCop
34
37
 
35
38
  def on_block(node)
36
39
  return unless DEFINE_METHODS.include?(node.method_name)
37
- return if defines_operator?(node)
40
+ return if exempt_defined_name?(node)
38
41
 
39
42
  check_arity(node)
40
43
  end
@@ -49,15 +52,22 @@ module RuboCop
49
52
  # modeled away, and a `Struct`/`Data` `initialize` just mirrors the members
50
53
  # the value object was declared with.
51
54
  def exempt?(node)
52
- node.operator_method? || allowed_initialize?(node)
55
+ node.operator_method? || allowed_initialize?(node) || allowed_name?(node.method_name)
53
56
  end
54
57
 
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)
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)
59
62
  defined_name = node.send_node.first_argument
60
- defined_name&.sym_type? && OPERATOR_METHOD_NAMES.include?(defined_name.value)
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)
61
71
  end
62
72
 
63
73
  def positional_arity(arguments)
@@ -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
@@ -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
 
@@ -16,6 +16,18 @@ module RuboCop
16
16
  # Depth is measured from each outermost call and reported once. There is no
17
17
  # autocorrection: choosing the intermediate name is a design decision.
18
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
+ #
19
31
  # @example Max: 1 (default)
20
32
  # # bad
21
33
  # foo(SomeClass.new(another("bar").chain))
@@ -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|
@@ -20,6 +20,30 @@ module RuboCop
20
20
  # outright. There is no autocorrection: renaming a method is a design
21
21
  # decision, and only its author knows the right plural.
22
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
+ #
23
47
  # @example
24
48
  # # bad
25
49
  # def user
@@ -48,7 +72,7 @@ module RuboCop
48
72
  # Methods whose result is an `Array` regardless of the receiver. Kept
49
73
  # deliberately short: anything whose return type follows its receiver
50
74
  # (`select` on a `Hash`) would turn this cop into a false-positive mill.
51
- ARRAY_METHODS = %i[
75
+ DEFAULT_ARRAY_METHODS = %i[
52
76
  map flat_map collect collect_concat to_a entries sort sort_by zip
53
77
  ].freeze
54
78
 
@@ -103,7 +127,11 @@ module RuboCop
103
127
  return true if node.array_type?
104
128
 
105
129
  call = node.any_block_type? ? node.send_node : node
106
- call.call_type? && ARRAY_METHODS.include?(call.method_name)
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)
107
135
  end
108
136
  end
109
137
  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)
@@ -9,12 +9,34 @@ module RuboCop
9
9
  # `context`/`it` description, a clearer example name, or another example --
10
10
  # not prose riding alongside the code.
11
11
  #
12
- # By default only `*_spec.rb` files are inspected (see `Include`). Magic
12
+ # By default only `*_spec.rb` files are inspected, and `spec/helpers` and
13
+ # `spec/support` are excluded -- they hold infrastructure, not specs. Magic
13
14
  # comments (`# frozen_string_literal: true`, `# encoding: ...`), RuboCop
14
15
  # directives (any `# rubocop:` comment), and shebangs are
15
16
  # never flagged; add further exemptions with `AllowedPatterns`. There is no
16
17
  # autocorrection: turning an explanation into a spec is a design decision.
17
18
  #
19
+ # == Configuration
20
+ #
21
+ # [+Include+] Files the cop runs on. Default: <tt>**/*_spec.rb</tt>.
22
+ # Broaden it to cover support files or a Minitest suite.
23
+ # [+Exclude+] Files the cop skips even when included. Default:
24
+ # <tt>**/spec/helpers/**/*</tt> and
25
+ # <tt>**/spec/support/**/*</tt>. Set it to <tt>[]</tt> to
26
+ # inspect those too.
27
+ # [+AllowedPatterns+] Regexps matched against the full comment text,
28
+ # leading +#+ included; a match is exempt.
29
+ # Default: none.
30
+ #
31
+ # Kaizo/SpecComment:
32
+ # inherit_mode:
33
+ # merge:
34
+ # - Include
35
+ # Include:
36
+ # - '**/*_test.rb' # Minitest too
37
+ # AllowedPatterns:
38
+ # - '\A#\s*@rbs' # rbs-inline type annotations
39
+ #
18
40
  # @example
19
41
  # # bad
20
42
  # it 'permits the request' do
@@ -5,16 +5,19 @@ module RuboCop
5
5
  # specifications, after the spec-skeleton naming law.
6
6
  #
7
7
  # An `it`/`specify`/`example` description must not contain a comma (a list
8
- # is several behaviors), a coordinating or conditional conjunction
9
- # (`Conjunctions` -- joined clauses are separate examples; a condition
10
- # belongs in a `context`), or code (`_ : # = { } ! [ ]`, a backtick, or a
11
- # nested quoted literal -- a description is prose, not identifiers or wire
12
- # values). Each rule is structural: it signals that one example is really
13
- # more than one, or that the assertion is leaking into the name.
8
+ # is several behaviors), a forbidden word (`ForbiddenWords` -- conjunctions
9
+ # join clauses that are separate examples; a conditional signals a hidden
10
+ # `context`), or code (`_ : # = { } ! [ ]`, a backtick, or a nested quoted
11
+ # literal -- a description is prose, not identifiers or wire values). Each
12
+ # rule is structural: it signals that one example is really more than one,
13
+ # or that the assertion is leaking into the name.
14
14
  #
15
15
  # A `context` description must not contain code, and must open with one of
16
- # `ContextPrefixes` (`when`/`with`/`without`/`after`). `describe` strings
17
- # name the unit under test and are exempt.
16
+ # `RequiredContextPrefixes` (`when`/`with`/`without`/`after`). `describe`
17
+ # strings name the unit under test and are exempt. An error class name
18
+ # (`Foo::Error`, `Timeout::DeadlineException`) reads as prose, not code:
19
+ # the error is part of the specified behavior and is what the user
20
+ # ultimately sees raised.
18
21
  #
19
22
  # Wording preferences that do not change the spec's structure (e.g. `should`
20
23
  # vs a present-tense verb) are out of scope -- see rubocop-rspec's
@@ -23,6 +26,32 @@ module RuboCop
23
26
  # There is no autocorrection: splitting an example, or extracting a
24
27
  # condition into a `context`, is a modelling decision for a human.
25
28
  #
29
+ # == Configuration
30
+ #
31
+ # [+ForbiddenWords+] Words that force a split when they appear in an
32
+ # example description, matched as whole words, case
33
+ # insensitively. Default: +and+, +but+, +or+, +nor+,
34
+ # +so+, +yet+, +when+, +whenever+, +if+, +unless+,
35
+ # +while+, +until+, +because+, +although+, +though+.
36
+ # [+RequiredContextPrefixes+] Words a `context` description may open
37
+ # with. Default: +when+, +with+, +without+,
38
+ # +after+.
39
+ # [+AllowedPatterns+] Regexps matched against the whole description; a
40
+ # match exempts it from every rule. The escape hatch
41
+ # for a description that must quote something
42
+ # code-shaped. Default: none.
43
+ # [+Include+] Files the cop runs on. Default: <tt>**/*_spec.rb</tt>.
44
+ #
45
+ # Kaizo/SpecDescriptionProse:
46
+ # inherit_mode:
47
+ # merge:
48
+ # - ForbiddenWords
49
+ # - AllowedPatterns
50
+ # ForbiddenWords:
51
+ # - given # flag `given ...` too
52
+ # AllowedPatterns:
53
+ # - 'Foo::Widget' # allow this one identifier
54
+ #
26
55
  # @example
27
56
  # # bad
28
57
  # it "renders the name, image, and flag"
@@ -39,6 +68,8 @@ module RuboCop
39
68
  # end
40
69
  #
41
70
  class SpecDescriptionProse < Base
71
+ include AllowedPattern
72
+
42
73
  EXAMPLE_METHODS = %i[
43
74
  it specify example fit xit fspecify xspecify fexample xexample
44
75
  ].freeze
@@ -46,8 +77,8 @@ module RuboCop
46
77
  RESTRICT_ON_SEND = (EXAMPLE_METHODS + CONTEXT_METHODS).freeze
47
78
 
48
79
  COMMA_MSG = "Split this example: its description contains a comma.".freeze
49
- CONJUNCTION_MSG = "Split this example: its description contains `%<word>s`; " \
50
- "use separate examples or a `context`.".freeze
80
+ FORBIDDEN_WORD_MSG = "Split this example: its description contains `%<word>s`; " \
81
+ "use separate examples or a `context`.".freeze
51
82
  CODE_MSG = "Write the description as prose; it contains code, not English.".freeze
52
83
  CONTEXT_CODE_MSG = "Write the context description as prose; it contains code, not English.".freeze
53
84
  CONTEXT_PREFIX_MSG = "Begin the context description with %<prefixes>s.".freeze
@@ -57,15 +88,19 @@ module RuboCop
57
88
  # Homographs like `even`/`given`/`regardless` are deliberately absent --
58
89
  # they collide with adjectives/nouns (`even numbers`) -- add them via
59
90
  # config if you want them.
60
- DEFAULT_CONJUNCTIONS = %w[
91
+ DEFAULT_FORBIDDEN_WORDS = %w[
61
92
  and but or nor so yet
62
93
  when whenever if unless while until because although though
63
94
  ].freeze
64
- DEFAULT_CONTEXT_PREFIXES = %w[when with without after].freeze
95
+ DEFAULT_REQUIRED_CONTEXT_PREFIXES = %w[when with without after].freeze
65
96
 
66
97
  CODE_CHARS = /[_:#={}!`\[\]]/
67
98
  NESTED_QUOTE = /(['"]).+\1/
68
99
 
100
+ # An error class name is part of the specified behavior -- it is what
101
+ # the user sees raised -- so it is masked to prose before the checks.
102
+ ERROR_CONSTANT = /(?:::)?\b(?:[A-Z]\w*::)*(?:[A-Z]\w*)?(?:Error|Exception)\b/
103
+
69
104
  # @!method description(node)
70
105
  def_node_matcher :description, <<~PATTERN
71
106
  (send nil? _ (str $_) ...)
@@ -74,8 +109,9 @@ module RuboCop
74
109
  def on_send(node)
75
110
  text = description(node)
76
111
  return unless text
112
+ return if matches_allowed_pattern?(text)
77
113
 
78
- message = violation(node.method_name, text)
114
+ message = violation(node.method_name, without_error_constants(text))
79
115
  return unless message
80
116
 
81
117
  add_offense(node.first_argument, message:)
@@ -83,6 +119,10 @@ module RuboCop
83
119
 
84
120
  private
85
121
 
122
+ def without_error_constants(text)
123
+ text.gsub(ERROR_CONSTANT, "error")
124
+ end
125
+
86
126
  def violation(method, text)
87
127
  if EXAMPLE_METHODS.include?(method)
88
128
  example_violation(text)
@@ -94,8 +134,8 @@ module RuboCop
94
134
  def example_violation(text)
95
135
  return COMMA_MSG if text.include?(",")
96
136
 
97
- word = forbidden(text, conjunctions)
98
- return format(CONJUNCTION_MSG, word:) if word
137
+ word = forbidden(text, forbidden_words)
138
+ return format(FORBIDDEN_WORD_MSG, word:) if word
99
139
 
100
140
  CODE_MSG if code?(text)
101
141
  end
@@ -116,19 +156,19 @@ module RuboCop
116
156
  end
117
157
 
118
158
  def prefix_regexp
119
- /\A\s*(?:#{context_prefixes.map { |prefix| Regexp.escape(prefix) }.join("|")})\b/i
159
+ /\A\s*(?:#{required_context_prefixes.map { |prefix| Regexp.escape(prefix) }.join("|")})\b/i
120
160
  end
121
161
 
122
162
  def quoted_prefixes
123
- context_prefixes.map { |prefix| "`#{prefix}`" }.join("/")
163
+ required_context_prefixes.map { |prefix| "`#{prefix}`" }.join("/")
124
164
  end
125
165
 
126
- def conjunctions
127
- cop_config.fetch("Conjunctions", DEFAULT_CONJUNCTIONS)
166
+ def forbidden_words
167
+ cop_config.fetch("ForbiddenWords", DEFAULT_FORBIDDEN_WORDS)
128
168
  end
129
169
 
130
- def context_prefixes
131
- cop_config.fetch("ContextPrefixes", DEFAULT_CONTEXT_PREFIXES)
170
+ def required_context_prefixes
171
+ cop_config.fetch("RequiredContextPrefixes", DEFAULT_REQUIRED_CONTEXT_PREFIXES)
132
172
  end
133
173
  end
134
174
  end