rubocop-rubomatic 2.1.0 → 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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +1 -0
- data/config/default.yml +35 -0
- data/config/rubomatic/rspec.yml +10 -0
- data/config/rubomatic/rubocop.yml +1 -0
- data/config/rubomatic/style.yml +11 -0
- data/docs/cops/rspec/README.md +7 -0
- data/docs/cops/rspec/describe_block_length/README.md +39 -0
- data/docs/cops/style/README.md +3 -0
- data/docs/cops/style/no_empty_join/README.md +30 -0
- data/docs/cops/style/no_empty_split/README.md +31 -0
- data/docs/cops/style/require_braces_around_hash/README.md +61 -0
- data/lib/rubocop/cop/rubomatic/rspec/describe_block_length.rb +68 -0
- data/lib/rubocop/cop/rubomatic/style/no_empty_join.rb +47 -0
- data/lib/rubocop/cop/rubomatic/style/no_empty_split.rb +47 -0
- data/lib/rubocop/cop/rubomatic/style/require_braces_around_hash.rb +200 -0
- data/lib/rubocop/cop/rubomatic_cops.rb +4 -0
- data/lib/rubocop/rubomatic/version.rb +1 -1
- metadata +14 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3b05a9731b1dfb618eb19eb33b00d4b5d63104cd18a3d5e7e71b2f822087c7e
|
|
4
|
+
data.tar.gz: eaca1d9e94d174c4ddac433aa6997f7b36e96b4750a44daca7bbeead97d735fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 219090ec2a3b3e880a8e6a5f25606fee0f2b725d61b2ba7d7c2e1ef6825c9562937d6692d2ac54d8431989407959e9d4d43e474094e91548c52a491104616d24
|
|
7
|
+
data.tar.gz: a1bf2e76d8bb68907b837d72bd728dcbc4f7d972440c6957f22c48dafd48711f4ac2a2e62528422e8bbacc673421a976c001ff4b80cf2b954a4c8006318cd1aa
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
|
|
16
|
+
## 2.1.1
|
|
17
|
+
|
|
18
|
+
### MISC
|
|
19
|
+
|
|
20
|
+
* Fix license metadata to be BSD-3 instead of MIT
|
|
21
|
+
|
|
5
22
|
## 2.1.0
|
|
6
23
|
|
|
7
24
|
### MISC
|
data/README.md
CHANGED
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'
|
data/config/rubomatic/style.yml
CHANGED
|
@@ -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
|
data/docs/cops/style/README.md
CHANGED
|
@@ -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'
|
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.
|
|
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-
|
|
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,21 +139,30 @@ 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
|
|
152
162
|
- lib/rubocop/rubomatic/version.rb
|
|
153
163
|
homepage: https://github.com/BrandsInsurance/rubocop-rubomatic/
|
|
154
164
|
licenses:
|
|
155
|
-
-
|
|
165
|
+
- BSD-3-Clause
|
|
156
166
|
metadata:
|
|
157
167
|
default_lint_roller_plugin: RuboCop::Rubomatic::Plugin
|
|
158
168
|
rubygems_mfa_required: 'true'
|
|
@@ -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.
|
|
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
|