Skip to content

Commit 71abb5d

Browse files
pawelmabbatsov
authored andcommitted
Add new consistent_either SupportedShorthandSyntax to Style/HashSyntax
This commit adds a new `consistent_either` to `EnforcedShorthandSyntax`. Related to feature request: #11436 This allows to use both explicit and shorthand syntax, similarly to existing `either` option, but with the difference that it enforces consistency within a single hash. This PR extends existing `consistent` option, and skips the `no_mixed_shorthand_syntax_check` offences when all values in the hash are ommitable.
1 parent f63df07 commit 71abb5d

File tree

5 files changed

+101
-8
lines changed

5 files changed

+101
-8
lines changed

config/default.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4069,6 +4069,8 @@ Style/HashSyntax:
40694069
- either
40704070
# forces use of the 3.1 syntax only if all values can be omitted in the hash.
40714071
- consistent
4072+
# allow either (implicit or explicit) syntax but enforce consistency within a single hash
4073+
- consistent_either
40724074
# Force hashes that have a symbol value to use hash rockets
40734075
UseHashRocketsWithSymbolValues: false
40744076
# Do not suggest { a?: 1 } over { :a? => 1 } in ruby19 style

lib/rubocop/cop/mixin/hash_shorthand_syntax.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,14 @@ def register_offense(node, message, replacement) # rubocop:disable Metrics/AbcSi
6767
end
6868

6969
def ignore_mixed_hash_shorthand_syntax?(hash_node)
70-
target_ruby_version <= 3.0 || enforced_shorthand_syntax != 'consistent' ||
70+
target_ruby_version <= 3.0 ||
71+
!%w[consistent consistent_either].include?(enforced_shorthand_syntax) ||
7172
!hash_node.hash_type?
7273
end
7374

7475
def ignore_hash_shorthand_syntax?(pair_node)
7576
target_ruby_version <= 3.0 || enforced_shorthand_syntax == 'either' ||
76-
enforced_shorthand_syntax == 'consistent' ||
77+
%w[consistent consistent_either].include?(enforced_shorthand_syntax) ||
7778
!pair_node.parent.hash_type?
7879
end
7980

@@ -172,6 +173,11 @@ def hash_with_values_that_cant_be_omitted?(hash_value_type_breakdown)
172173
hash_value_type_breakdown[:value_needed]&.any?
173174
end
174175

176+
def ignore_explicit_ommitable_hash_shorthand_syntax?(hash_value_type_breakdown)
177+
hash_value_type_breakdown.keys == [:value_omittable] &&
178+
enforced_shorthand_syntax == 'consistent_either'
179+
end
180+
175181
def each_omitted_value_pair(hash_value_type_breakdown, &block)
176182
hash_value_type_breakdown[:value_omitted]&.each(&block)
177183
end
@@ -198,6 +204,7 @@ def mixed_shorthand_syntax_check(hash_value_type_breakdown)
198204

199205
def no_mixed_shorthand_syntax_check(hash_value_type_breakdown)
200206
return if hash_with_values_that_cant_be_omitted?(hash_value_type_breakdown)
207+
return if ignore_explicit_ommitable_hash_shorthand_syntax?(hash_value_type_breakdown)
201208

202209
each_omittable_value_pair(hash_value_type_breakdown) do |pair_node|
203210
hash_key_source = pair_node.key.source

lib/rubocop/cop/style/hash_syntax.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ module Style
2929
# * never - forces use of explicit hash literal value
3030
# * either - accepts both shorthand and explicit use of hash literal value
3131
# * consistent - forces use of the 3.1 syntax only if all values can be omitted in the hash
32+
# * consistent_either - accepts both shorthand and explicit use of hash literal value,
33+
# but they must be consistent
3234
#
3335
# @example EnforcedStyle: ruby19 (default)
3436
# # bad
@@ -110,6 +112,22 @@ module Style
110112
# # good - can't omit `baz`
111113
# {foo: foo, bar: baz}
112114
#
115+
# @example EnforcedShorthandSyntax: consistent_either
116+
#
117+
# # good - `foo` and `bar` values can be omitted, but they are consistent, so it's accepted
118+
# {foo: foo, bar: bar}
119+
#
120+
# # bad - `bar` value can be omitted
121+
# {foo:, bar: bar}
122+
#
123+
# # bad - mixed syntaxes
124+
# {foo:, bar: baz}
125+
#
126+
# # good
127+
# {foo:, bar:}
128+
#
129+
# # good - can't omit `baz`
130+
# {foo: foo, bar: baz}
113131
class HashSyntax < Base
114132
include ConfigurableEnforcedStyle
115133
include HashShorthandSyntax

spec/rubocop/cli/auto_gen_config_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
15001500
.to eq(<<~YAML)
15011501
# Configuration parameters: EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
15021502
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
1503-
# SupportedShorthandSyntax: always, never, either, consistent
1503+
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
15041504
Style/HashSyntax:
15051505
EnforcedStyle: hash_rockets
15061506
YAML
@@ -1514,7 +1514,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
15141514
.to eq(<<~YAML)
15151515
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
15161516
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
1517-
# SupportedShorthandSyntax: always, never, either, consistent
1517+
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
15181518
Style/HashSyntax:
15191519
Exclude:
15201520
- 'example1.rb'
@@ -1531,7 +1531,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
15311531
.to eq(<<~YAML)
15321532
# Configuration parameters: EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
15331533
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
1534-
# SupportedShorthandSyntax: always, never, either, consistent
1534+
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
15351535
Style/HashSyntax:
15361536
Exclude:
15371537
- 'example1.rb'
@@ -1546,7 +1546,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
15461546
.to eq(<<~YAML)
15471547
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
15481548
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
1549-
# SupportedShorthandSyntax: always, never, either, consistent
1549+
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
15501550
Style/HashSyntax:
15511551
Exclude:
15521552
- 'example1.rb'
@@ -1583,7 +1583,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
15831583
.to eq(<<~YAML)
15841584
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
15851585
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
1586-
# SupportedShorthandSyntax: always, never, either, consistent
1586+
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
15871587
Style/HashSyntax:
15881588
Exclude:
15891589
- 'example1.rb'

spec/rubocop/cop/style/hash_syntax_spec.rb

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1606,7 +1606,7 @@ def buz(foo:, bar:); end
16061606
end
16071607
end
16081608

1609-
context 'configured to disallow mixing of implicit and explicit hash literal value' do
1609+
context 'configured to disallow mixing of implicit and explicit hash literal value, but prefers shorthand syntax whenever possible' do
16101610
let(:cop_config) do
16111611
{
16121612
'EnforcedStyle' => 'ruby19',
@@ -1677,4 +1677,70 @@ def buz(foo:, bar:); end
16771677
end
16781678
end
16791679
end
1680+
1681+
context 'configured to disallow mixing of implicit and explicit hash literal value' do
1682+
let(:cop_config) do
1683+
{
1684+
'EnforcedStyle' => 'ruby19',
1685+
'SupportedStyles' => %w[ruby19 hash_rockets],
1686+
'EnforcedShorthandSyntax' => 'consistent_either'
1687+
}
1688+
end
1689+
1690+
context 'Ruby >= 3.1', :ruby31 do
1691+
it 'does not register an offense when all hash values are omitted' do
1692+
expect_no_offenses(<<~RUBY)
1693+
{foo:, bar:}
1694+
RUBY
1695+
end
1696+
1697+
it 'registers an offense when some hash values are omitted but they can all be omitted' do
1698+
expect_offense(<<~RUBY)
1699+
{foo:, bar: bar}
1700+
^^^ Do not mix explicit and implicit hash values. Omit the hash value.
1701+
RUBY
1702+
1703+
expect_correction(<<~RUBY)
1704+
{foo:, bar:}
1705+
RUBY
1706+
end
1707+
1708+
it 'registers an offense when some hash values are omitted but they cannot all be omitted' do
1709+
expect_offense(<<~RUBY)
1710+
{foo:, bar: baz}
1711+
^^^ Do not mix explicit and implicit hash values. Include the hash value.
1712+
RUBY
1713+
1714+
expect_correction(<<~RUBY)
1715+
{foo: foo, bar: baz}
1716+
RUBY
1717+
end
1718+
1719+
it 'does not register an offense when all hash values are present, but no values can be omitted' do
1720+
expect_no_offenses(<<~RUBY)
1721+
{foo: bar, bar: foo}
1722+
RUBY
1723+
end
1724+
1725+
it 'does not register an offense when all hash values are present, but only some values can be omitted' do
1726+
expect_no_offenses(<<~RUBY)
1727+
{foo: baz, bar: bar}
1728+
RUBY
1729+
end
1730+
1731+
it 'does not register an offense when all hash values are present, but can all be omitted' do
1732+
expect_no_offenses(<<~RUBY)
1733+
{foo: foo, bar: bar}
1734+
RUBY
1735+
end
1736+
end
1737+
1738+
context 'Ruby <= 3.0', :ruby30, unsupported_on: :prism do
1739+
it 'does not register an offense when hash key and hash value are the same' do
1740+
expect_no_offenses(<<~RUBY)
1741+
{foo: foo, bar: bar}
1742+
RUBY
1743+
end
1744+
end
1745+
end
16801746
end

0 commit comments

Comments
 (0)