rubocop-rubomatic 2.1.1 → 2.2.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: 1b38508a5a6a0a201969e34ab81bfcd8aba925a8c3d657400bba7e31a40c36c4
4
- data.tar.gz: fe005603fa46c66d9c86963820a9c9a4eb8c835ba928240ebbe39e86095a3847
3
+ metadata.gz: c3b05a9731b1dfb618eb19eb33b00d4b5d63104cd18a3d5e7e71b2f822087c7e
4
+ data.tar.gz: eaca1d9e94d174c4ddac433aa6997f7b36e96b4750a44daca7bbeead97d735fb
5
5
  SHA512:
6
- metadata.gz: ed2af4ecdf6f3ce9a05ea9d95aa34aa1d4b29739fcb5e6dba413e7ec465e03b47619a78e33e06435039b46bb252acdf4bef3baa79c2a3ac397293a0db417acb7
7
- data.tar.gz: e7b2ecd1aba276241814f8ff4b4bf77998362557fe53a0dbeef31df742729786572b3bb681314e0a797d784bb24dc116a51112e007eef72d35fd7ebcd3245530
6
+ metadata.gz: 219090ec2a3b3e880a8e6a5f25606fee0f2b725d61b2ba7d7c2e1ef6825c9562937d6692d2ac54d8431989407959e9d4d43e474094e91548c52a491104616d24
7
+ data.tar.gz: a1bf2e76d8bb68907b837d72bd728dcbc4f7d972440c6957f22c48dafd48711f4ac2a2e62528422e8bbacc673421a976c001ff4b80cf2b954a4c8006318cd1aa
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased changes
4
4
 
5
+ ## 2.2.0
6
+
7
+ ### RSpec
8
+
9
+ * Added `Rubomatic/RSpec/DescribeBlockLength`
10
+
11
+ ### Style
12
+
13
+ * Added `Rubomatic/Style/NoEmptyJoin`
14
+ * Added `Rubomatic/Style/NoEmptySplit`
15
+
5
16
  ## 2.1.1
6
17
 
7
18
  ### MISC
data/README.md CHANGED
@@ -75,6 +75,7 @@ Rubomatic/Style/DisallowedMethods:
75
75
  ## Departments
76
76
 
77
77
  * [`Layout`](./docs/cops/layout/README.md)
78
+ * [`RSpec`](./docs/cops/rspec/README.md)
78
79
  * [`Style`](./docs/cops/style/README.md)
79
80
 
80
81
  ## Contributing
data/config/default.yml CHANGED
@@ -3,5 +3,40 @@ Rubomatic/Layout/MultilineArrayLineBreaks:
3
3
  AllowMultilineFinalElement: true
4
4
  AllowPercentArray: true
5
5
 
6
+ Rubomatic/RSpec/DescribeBlockLength:
7
+ Description: 'Limit nested `describe` block length so long ones get extracted into their own spec file.'
8
+ Enabled: pending
9
+ VersionAdded: '<<next>>'
10
+ CountComments: false # count full line comments?
11
+ Max: 300
12
+ CountAsOne:
13
+ - 'array'
14
+ - 'heredoc'
15
+ - 'hash'
16
+ Include:
17
+ - 'spec/**/*.rb'
18
+
6
19
  Rubomatic/Style/DisallowedMethods:
7
20
  Enabled: true
21
+
22
+ Rubomatic/Style/NoEmptyJoin:
23
+ Description: 'Require a separator argument for `Array#join`.'
24
+ Enabled: pending
25
+ Safe: false
26
+ SafeAutoCorrect: false
27
+ VersionAdded: '<<next>>'
28
+
29
+ Rubomatic/Style/NoEmptySplit:
30
+ Description: 'Require a separator argument for `String#split`.'
31
+ Enabled: pending
32
+ Safe: false
33
+ SafeAutoCorrect: false
34
+ VersionAdded: '<<next>>'
35
+
36
+ Rubomatic/Style/RequireBracesAroundHash:
37
+ Description: 'Require `{}` around a hash passed to a method that takes an options hash instead of keywords.'
38
+ Enabled: pending
39
+ SafeAutoCorrect: false
40
+ VersionAdded: '<<next>>'
41
+ Exclude:
42
+ - '**/db/**/*.rb'
@@ -0,0 +1,10 @@
1
+ Rubomatic/RSpec/DescribeBlockLength:
2
+ Enabled: true
3
+ CountComments: false
4
+ Max: 300
5
+ CountAsOne:
6
+ - 'array'
7
+ - 'heredoc'
8
+ - 'hash'
9
+ Include:
10
+ - 'spec/**/*.rb'
@@ -2,4 +2,5 @@ plugins: rubocop-rubomatic
2
2
 
3
3
  inherit_from:
4
4
  - ./layout.yml
5
+ - ./rspec.yml
5
6
  - ./style.yml
@@ -1,2 +1,13 @@
1
1
  Rubomatic/Style/DisallowedMethods:
2
2
  Enabled: true
3
+
4
+ Rubomatic/Style/NoEmptyJoin:
5
+ Enabled: true
6
+
7
+ Rubomatic/Style/NoEmptySplit:
8
+ Enabled: true
9
+
10
+ Rubomatic/Style/RequireBracesAroundHash:
11
+ Enabled: true
12
+ Exclude:
13
+ - '**/db/**/*.rb'
@@ -0,0 +1,7 @@
1
+ # RSpec
2
+
3
+ RSpec cops check for stylistic consistency of your specs
4
+
5
+ ## Cops
6
+
7
+ * [`RSpec/DescribeBlockLength`](./describe_block_length/README.md)
@@ -0,0 +1,39 @@
1
+ # `RSpec/DescribeBlockLength`
2
+
3
+ ## Description
4
+
5
+ Checks if a nested `describe` block is longer than `Max` lines. A long nested `describe` should be extracted into its
6
+ own spec file. Top-level `describe` blocks are not checked; `Metrics/BlockLength` covers those. Blocks inside
7
+ `shared_examples`, `shared_examples_for`, and `shared_context` are skipped.
8
+
9
+ Subclass of `Metrics/BlockLength`, so `CountComments` and `CountAsOne` work the same way.
10
+
11
+ ## Examples
12
+
13
+ ```ruby
14
+ # Bad
15
+ # issue_spec.rb
16
+ RSpec.describe(PMS::Issue) do
17
+ describe('.create!') do
18
+ # 350 lines of tests
19
+ end
20
+ end
21
+
22
+ # Good
23
+ # issue/create_spec.rb
24
+ RSpec.describe(PMS::Issue, '.create!') do
25
+ # 350 lines of tests
26
+ end
27
+ ```
28
+
29
+ ## Configurable Attributes
30
+
31
+ | Name | Default value | Configurable values |
32
+ | ------------- | ------------------------------ | ------------------- |
33
+ | Max | 300 | Integer |
34
+ | CountComments | false | Boolean |
35
+ | CountAsOne | `['array', 'heredoc', 'hash']` | Array |
36
+
37
+ ## References
38
+
39
+ https://github.com/BrandsInsurance/expert-chainsaw/issues/1741
@@ -5,3 +5,6 @@ Style cops check for stylistic consistency of your code
5
5
  ## Cops
6
6
 
7
7
  * [`Style/DisallowedMethods`](./disallowed_methods/README.md)
8
+ * [`Style/NoEmptyJoin`](./no_empty_join/README.md)
9
+ * [`Style/NoEmptySplit`](./no_empty_split/README.md)
10
+ * [`Style/RequireBracesAroundHash`](./require_braces_around_hash/README.md)
@@ -0,0 +1,30 @@
1
+ # `Style/NoEmptyJoin`
2
+
3
+ ## Description
4
+
5
+ `Array#join` should always be passed a separator. Without one, Ruby falls back to `$,`, and only uses `''` when `$,` is
6
+ also `nil`. Passing the separator explicitly makes the intent clear and doesn't depend on global state.
7
+
8
+ Autocorrection (`join` -> `join('')`) is unsafe because the receiver may not be an `Array` (e.g. `Thread#join`).
9
+
10
+ ## Examples
11
+
12
+ ```ruby
13
+ # Bad
14
+ [1, 2, 3].join
15
+ [1, 2, 3].join()
16
+ the_var&.join
17
+
18
+ # Good
19
+ [1, 2, 3].join('')
20
+ [1, 2, 3].join(', ')
21
+ the_var&.join(' ')
22
+ ```
23
+
24
+ ## Configurable Attributes
25
+
26
+ There are no configurable attributes
27
+
28
+ ## References
29
+
30
+ * https://github.com/BrandsInsurance/expert-chainsaw/issues/214
@@ -0,0 +1,31 @@
1
+ # `Style/NoEmptySplit`
2
+
3
+ ## Description
4
+
5
+ `String#split` should always be passed a separator. Without one, Ruby falls back to `$;`, which is normally `nil`
6
+ (split on whitespace) but can be overridden, and this behavior has changed between Ruby versions. Passing the separator
7
+ explicitly makes the intent clear and doesn't depend on global state.
8
+
9
+ Autocorrection (`split` -> `split(' ')`) is unsafe because the receiver may not be a `String`.
10
+
11
+ ## Examples
12
+
13
+ ```ruby
14
+ # Bad
15
+ 'the string'.split
16
+ 'the string'.split()
17
+ the_var&.split
18
+
19
+ # Good
20
+ 'the string'.split(' ')
21
+ 'a,b,c'.split(',')
22
+ the_var&.split(' ')
23
+ ```
24
+
25
+ ## Configurable Attributes
26
+
27
+ There are no configurable attributes
28
+
29
+ ## References
30
+
31
+ * https://github.com/BrandsInsurance/expert-chainsaw/issues/213
@@ -0,0 +1,61 @@
1
+ # `Style/RequireBracesAroundHash`
2
+
3
+ ## Description
4
+
5
+ Requires `{}` around a hash argument when the called method takes it as a positional options hash instead of keyword
6
+ arguments. The braces tell the caller it's an options hash and not keywords.
7
+
8
+ The method is looked up in the project index, so the cop does nothing unless `AllCops/UseProjectIndex` is enabled. A
9
+ call is only flagged when the method is found and none of its definitions accept keywords (`a:`, `a: 1`, `**kwargs`, or
10
+ `...`). Calls whose method can't be found (unknown methods, unresolved constants, locals, and chained receivers) are not
11
+ flagged.
12
+
13
+ The method is looked up on:
14
+
15
+ * a constant receiver's class methods (`Service.call`), or its `initialize` for `new`
16
+ * the enclosing class for bare and `self.` calls, on the class side inside the class body, `def self.`, and
17
+ `class << self`, and on the instance side inside instance methods
18
+ * `Object` for bare calls outside a class
19
+
20
+ Autocorrection is unsafe because wrapping the hash changes behavior for `ruby2_keywords` delegators, which pass a
21
+ braceless hash on as keywords but a braced hash as a positional argument.
22
+
23
+ ## Examples
24
+
25
+ ```ruby
26
+ def foo(options = { a: 0 }); end
27
+ def bar(a: 0); end
28
+
29
+ class Service
30
+ def self.call(options = {}); end
31
+
32
+ def initialize(options = {}); end
33
+ end
34
+
35
+ # Bad
36
+ foo(a: 1)
37
+ foo a: 1
38
+ foo(**options)
39
+ Service.call(a: 1)
40
+ Service.new(a: 1)
41
+
42
+ # Good
43
+ foo({ a: 1 })
44
+ foo({ **options })
45
+ Service.call({ a: 1 })
46
+ Service.new({ a: 1 })
47
+
48
+ # Good - `bar` takes keywords
49
+ bar(a: 1)
50
+ bar(**options)
51
+ ```
52
+
53
+ ## Configurable Attributes
54
+
55
+ There are no configurable attributes
56
+
57
+ Requires `AllCops/UseProjectIndex: true` and the `rubydex` gem (Ruby 3.2+)
58
+
59
+ ## References
60
+
61
+ * https://github.com/BrandsInsurance/expert-chainsaw/issues/150
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rubomatic::RSpec
6
+ # Checks if a nested `describe` block is longer than `Max` lines. A long nested `describe` should be extracted
7
+ # into its own spec file. Top-level `describe` blocks are not checked; `Metrics/BlockLength` covers those.
8
+ # Blocks inside `shared_examples`, `shared_examples_for`, and `shared_context` are skipped.
9
+ # Configured the same way as `Metrics/BlockLength`.
10
+ #
11
+ # @example Max: 300 (default)
12
+ # # bad - issue_spec.rb
13
+ # RSpec.describe(PMS::Issue) do
14
+ # describe('.create!') do
15
+ # # 350 lines of tests
16
+ # end
17
+ # end
18
+ #
19
+ # # good - issue/create_spec.rb
20
+ # RSpec.describe(PMS::Issue, '.create!') do
21
+ # # 350 lines of tests
22
+ # end
23
+ #
24
+ class DescribeBlockLength < RuboCop::Cop::Metrics::BlockLength
25
+ LABEL = 'Nested `describe` block'
26
+ EXAMPLE_GROUPS = %i[describe context feature].to_set.freeze
27
+ SHARED_GROUPS = %i[shared_examples shared_examples_for shared_context].to_set.freeze
28
+
29
+ # @!method describe?(node)
30
+ def_node_matcher :describe?, <<~PATTERN
31
+ (any_block (send {nil? (const {nil? cbase} :RSpec)} :describe ...) ...)
32
+ PATTERN
33
+
34
+ # @see super
35
+ def on_block(node)
36
+ return unless describe?(node)
37
+ return unless nested?(node)
38
+
39
+ super
40
+ end
41
+ # Re-aliased so they hit this `on_block`, not the parent's
42
+ alias on_numblock on_block
43
+ alias on_itblock on_block
44
+
45
+ # Public because `Lint/UnusedPrivateMethod` can't see that the parent calls it
46
+ #
47
+ # @see super
48
+ #
49
+ def cop_label
50
+ LABEL
51
+ end
52
+
53
+ private
54
+
55
+ # Shared groups are skipped; they belong in their own file instead
56
+ #
57
+ # @return [Boolean] is the block inside another example group and not a shared group
58
+ #
59
+ def nested?(node)
60
+ ancestors = node.each_ancestor(:any_block).map(&:method_name)
61
+ return false if ancestors.any? { |name| SHARED_GROUPS.include?(name) }
62
+
63
+ ancestors.any? { |name| EXAMPLE_GROUPS.include?(name) }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rubomatic::Style
6
+ # @safety
7
+ # This cop is unsafe because the receiver may not be an `Array`
8
+ # (e.g. `Thread#join`), so offenses may be false positives and adding a
9
+ # separator changes behavior.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # [1, 2, 3].join
14
+ #
15
+ # # good
16
+ # [1, 2, 3].join('args')
17
+ #
18
+ class NoEmptyJoin < Base
19
+ MSG = 'Pass an argument `Array#join` instead of no arguments.'
20
+
21
+ # `on_send` will only be called if the method name is in this list
22
+ RESTRICT_ON_SEND = %i[join].freeze
23
+
24
+ extend AutoCorrector
25
+
26
+ # @!method argless_join?(node)
27
+ def_node_matcher :argless_join?, <<~PATTERN
28
+ (call !nil? :join)
29
+ PATTERN
30
+
31
+ # Called on every `send` node (method call) while walking the AST.
32
+ # TODO: remove this method if inspecting `send` nodes is unneeded for your cop.
33
+ # By default, this is aliased to `on_csend` as well to handle method calls
34
+ # with safe navigation, remove the alias if this is unnecessary.
35
+ # If kept, ensure your tests cover safe navigation as well!
36
+ def on_send(node)
37
+ return unless argless_join?(node)
38
+
39
+ add_offense(node) do |corrector|
40
+ corrector.replace(node.loc.selector.join(node.source_range.end), "join('')")
41
+ end
42
+ end
43
+ alias on_csend on_send
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rubomatic::Style
6
+ # @safety
7
+ # This cop is unsafe because the receiver may not be a `String`
8
+ # (e.g. `Banana#split`), so offenses may be false positives and adding a
9
+ # separator changes behavior.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # 'the string'.split
14
+ #
15
+ # # good
16
+ # 'the string'.split(' ')
17
+ #
18
+ class NoEmptySplit < Base
19
+ MSG = 'Pass an argument `String#split` instead of no arguments.'
20
+
21
+ # `on_send` will only be called if the method name is in this list
22
+ RESTRICT_ON_SEND = %i[split].freeze
23
+
24
+ extend AutoCorrector
25
+
26
+ # @!method argless_split?(node)
27
+ def_node_matcher :argless_split?, <<~PATTERN
28
+ (call !nil? :split)
29
+ PATTERN
30
+
31
+ # Called on every `send` node (method call) while walking the AST.
32
+ # TODO: remove this method if inspecting `send` nodes is unneeded for your cop.
33
+ # By default, this is aliased to `on_csend` as well to handle method calls
34
+ # with safe navigation, remove the alias if this is unnecessary.
35
+ # If kept, ensure your tests cover safe navigation as well!
36
+ def on_send(node)
37
+ return unless argless_split?(node)
38
+
39
+ add_offense(node) do |corrector|
40
+ corrector.replace(node.loc.selector.join(node.source_range.end), "split(' ')")
41
+ end
42
+ end
43
+ alias on_csend on_send
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rubomatic::Style
6
+ # Requires `{}` around a hash argument when the called method takes it as a positional options hash instead of
7
+ # keyword arguments. The braces tell the caller it's an options hash and not keywords.
8
+ #
9
+ # The method is looked up in the project index (`AllCops/UseProjectIndex`), so the cop does nothing without it.
10
+ # A call is only flagged when the method is found and none of its definitions accept keywords (`a:`, `a: 1`,
11
+ # `**kwargs`, or `...`). Calls whose method can't be found (unknown methods, unresolved constants, locals, and
12
+ # chained receivers) are not flagged.
13
+ #
14
+ # The method is looked up on:
15
+ #
16
+ # - a constant receiver's class methods (`Service.call`), or its `initialize` for `new`
17
+ # - the enclosing class for bare and `self.` calls, on the class side inside the class body, `def self.`, and
18
+ # `class << self`, and on the instance side inside instance methods
19
+ # - `Object` for bare calls outside a class
20
+ #
21
+ # @safety
22
+ # Autocorrection is unsafe because wrapping the hash changes behavior for `ruby2_keywords` delegators, which
23
+ # pass a braceless hash on as keywords but a braced hash as a positional argument.
24
+ #
25
+ # @example
26
+ # def foo(options = { a: 0 }); end
27
+ # def bar(a: 0); end
28
+ #
29
+ # # bad
30
+ # foo(a: 1)
31
+ # foo a: 1
32
+ # foo(**options)
33
+ #
34
+ # # good
35
+ # foo({ a: 1 })
36
+ # foo({ **options })
37
+ #
38
+ # # good - `bar` takes keywords
39
+ # bar(a: 1)
40
+ # bar(**options)
41
+ #
42
+ # @example
43
+ # class Service
44
+ # def self.call(options = {}); end
45
+ #
46
+ # def initialize(options = {}); end
47
+ # end
48
+ #
49
+ # # bad
50
+ # Service.call(a: 1)
51
+ # Service.new(a: 1)
52
+ #
53
+ # # good
54
+ # Service.call({ a: 1 })
55
+ # Service.new({ a: 1 })
56
+ #
57
+ class RequireBracesAroundHash < Base
58
+ MSG = 'Wrap options hash in `{}`.'
59
+
60
+ extend AutoCorrector
61
+ include ProjectIndexHelp
62
+
63
+ # :nodoc:
64
+ def on_send(node)
65
+ return unless project_index
66
+ # `Foo[a: 1]` has no parens to add, so the correction would swallow the `]`
67
+ return if node.method?(:[]) || node.method?(:[]=)
68
+
69
+ hashes = node.arguments.select { |arg| arg.hash_type? && !arg.braces? }
70
+ return if hashes.empty?
71
+
72
+ definitions = method_definitions(node)
73
+ return if definitions.empty?
74
+ return if accepts_keywords?(definitions)
75
+
76
+ hashes.each do |arg|
77
+ add_offense(arg) do |corrector|
78
+ wrap_in_braces(corrector, node, arg)
79
+ end
80
+ end
81
+ end
82
+ alias on_csend on_send
83
+
84
+ private
85
+
86
+ # Whether any indexed definition of the called method accepts keyword arguments
87
+ #
88
+ # @param definitions [Array<Rubydex::MethodDefinition>]
89
+ #
90
+ # @return [Boolean]
91
+ #
92
+ def accepts_keywords?(definitions)
93
+ definitions.any? do |definition|
94
+ definition.signatures.any? { |signature| keyword_signature?(signature) }
95
+ end
96
+ end
97
+
98
+ # @return [Boolean] does the signature take `a:`, `a: 1`, `**kwargs`, or `...`
99
+ def keyword_signature?(signature)
100
+ !signature.keyword_parameters.empty? ||
101
+ !signature.optional_keyword_parameters.empty? ||
102
+ !signature.rest_keyword_parameter.nil? ||
103
+ !signature.forward_parameter.nil?
104
+ end
105
+
106
+ # The indexed definitions of the called method, or `[]` when it can't be found
107
+ #
108
+ # @param node [RuboCop::AST::SendNode]
109
+ #
110
+ # @return [Array<Rubydex::MethodDefinition>]
111
+ #
112
+ def method_definitions(node)
113
+ method = find_method(node)
114
+ return [] if method.nil?
115
+
116
+ method.definitions.grep(Rubydex::MethodDefinition)
117
+ end
118
+
119
+ # Looks up the called method on the receiver's class, or on the enclosing class for bare and `self.` calls.
120
+ # Class-side calls look on the singleton class, except `new`, which looks up `initialize`
121
+ #
122
+ # @param node [RuboCop::AST::SendNode]
123
+ #
124
+ # @return [Rubydex::Method, nil]
125
+ #
126
+ def find_method(node)
127
+ namespace, singleton = receiver_scope(node)
128
+ return unless namespace.is_a?(Rubydex::Namespace)
129
+
130
+ if singleton && node.method?(:new)
131
+ namespace.find_member('initialize()')
132
+ elsif singleton
133
+ indexed_singleton_member(namespace, "#{node.method_name}()")
134
+ else
135
+ namespace.find_member("#{node.method_name}()")
136
+ end
137
+ end
138
+
139
+ # The namespace to look the method up in, and whether the lookup is on its class side
140
+ #
141
+ # @param node [RuboCop::AST::SendNode]
142
+ #
143
+ # @return [Array(Rubydex::Declaration, Boolean), nil]
144
+ #
145
+ def receiver_scope(node)
146
+ receiver = node.receiver
147
+ return [resolve_constant_in_index(receiver), true] if receiver&.const_type?
148
+ return if receiver && !receiver.self_type?
149
+
150
+ scope_node = node.each_ancestor(:class, :module, :sclass).first
151
+ return [project_index['Object'], false] if scope_node.nil?
152
+ return [resolve_constant_in_index(scope_node.identifier), class_side?(node)] unless scope_node.sclass_type?
153
+ return unless scope_node.identifier.self_type?
154
+
155
+ class_node = scope_node.each_ancestor(:class, :module).first
156
+ [class_node && resolve_constant_in_index(class_node.identifier), true]
157
+ end
158
+
159
+ # @return [Boolean] is `self` the class itself (class body or `def self.`), not an instance
160
+ def class_side?(node)
161
+ !node.each_ancestor(:def, :defs).first&.def_type?
162
+ end
163
+
164
+ # Wraps the hash in braces, keeping the layout of the call
165
+ #
166
+ # @param corrector [RuboCop::Cop::Corrector]
167
+ # @param node [RuboCop::AST::SendNode]
168
+ # @param hash [RuboCop::AST::HashNode]
169
+ #
170
+ # @return [void]
171
+ #
172
+ def wrap_in_braces(corrector, node, hash)
173
+ if !node.parenthesized?
174
+ # `create :user, name: name` -> `create(:user, { name: name })`
175
+ corrector.replace(node.loc.selector.end.join(node.first_argument.source_range.begin), '(')
176
+ corrector.insert_before(hash, '{ ')
177
+ corrector.insert_after(hash, ' }')
178
+ corrector.insert_after(node.last_argument, ')')
179
+ elsif hash.first_line == node.loc.begin.line
180
+ # `User.new(name: name)` / `User.create(name: name,\n password: password)`
181
+ corrector.insert_before(hash, '{ ')
182
+ corrector.insert_after(hash, ' }')
183
+ elsif node.arguments.one?
184
+ # `User.new(\n name: name,\n)` -> `User.new({\n name: name,\n})`
185
+ corrector.insert_after(node.loc.begin, '{')
186
+ corrector.insert_before(node.loc.end, '}')
187
+ else
188
+ # `create(\n :user,\n name: name,\n)` -> hash gets its own `{` / `}` lines, pairs indented one level
189
+ indent = ' ' * hash.loc.column
190
+ corrector.insert_before(hash, "{\n#{indent} ")
191
+ hash.children.drop(1).each do |child|
192
+ corrector.insert_before(child, ' ') if begins_its_line?(child.source_range)
193
+ end
194
+ corrector.insert_after(hash, ",\n#{indent}}")
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'rubomatic/layout/multiline_array_line_breaks'
4
+ require_relative 'rubomatic/rspec/describe_block_length'
4
5
  require_relative 'rubomatic/style/disallowed_methods'
6
+ require_relative 'rubomatic/style/no_empty_join'
7
+ require_relative 'rubomatic/style/no_empty_split'
8
+ require_relative 'rubomatic/style/require_braces_around_hash'
@@ -4,8 +4,8 @@ module RuboCop
4
4
  module Rubomatic
5
5
  module VERSION
6
6
  MAJOR = 2
7
- MINOR = 1
8
- PATCH = 1
7
+ MINOR = 2
8
+ PATCH = 0
9
9
  TINY = PATCH
10
10
 
11
11
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rubomatic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lint_roller
@@ -131,6 +131,7 @@ files:
131
131
  - config/rspec.yml
132
132
  - config/rubocop.yml
133
133
  - config/rubomatic/layout.yml
134
+ - config/rubomatic/rspec.yml
134
135
  - config/rubomatic/rubocop.yml
135
136
  - config/rubomatic/style.yml
136
137
  - config/security.yml
@@ -138,14 +139,23 @@ files:
138
139
  - config/thread_safety.yml
139
140
  - docs/cops/layout/README.md
140
141
  - docs/cops/layout/multiline_array_line_breaks/README.md
142
+ - docs/cops/rspec/README.md
143
+ - docs/cops/rspec/describe_block_length/README.md
141
144
  - docs/cops/style/README.md
142
145
  - docs/cops/style/disallowed_methods/README.md
146
+ - docs/cops/style/no_empty_join/README.md
147
+ - docs/cops/style/no_empty_split/README.md
148
+ - docs/cops/style/require_braces_around_hash/README.md
143
149
  - lib/rubocop-rubomatic.rb
144
150
  - lib/rubocop/cop/rubomatic/generator.rb
145
151
  - lib/rubocop/cop/rubomatic/generator/cop_readme_injector.rb
146
152
  - lib/rubocop/cop/rubomatic/generator/dept_readme_injector.rb
147
153
  - lib/rubocop/cop/rubomatic/layout/multiline_array_line_breaks.rb
154
+ - lib/rubocop/cop/rubomatic/rspec/describe_block_length.rb
148
155
  - lib/rubocop/cop/rubomatic/style/disallowed_methods.rb
156
+ - lib/rubocop/cop/rubomatic/style/no_empty_join.rb
157
+ - lib/rubocop/cop/rubomatic/style/no_empty_split.rb
158
+ - lib/rubocop/cop/rubomatic/style/require_braces_around_hash.rb
149
159
  - lib/rubocop/cop/rubomatic_cops.rb
150
160
  - lib/rubocop/rubomatic.rb
151
161
  - lib/rubocop/rubomatic/plugin.rb
@@ -174,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
184
  - !ruby/object:Gem::Version
175
185
  version: '0'
176
186
  requirements: []
177
- rubygems_version: 3.5.11
187
+ rubygems_version: 3.5.22
178
188
  signing_key:
179
189
  specification_version: 4
180
190
  summary: Custom cops and shared config for BrandsInsurance