Skip to content

Commit

Permalink
Add new consistent_either SupportedShorthandSyntax to Style/HashSyntax
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pawelma authored and bbatsov committed May 21, 2024
1 parent f63df07 commit 71abb5d
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 8 deletions.
2 changes: 2 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4069,6 +4069,8 @@ Style/HashSyntax:
- either
# forces use of the 3.1 syntax only if all values can be omitted in the hash.
- consistent
# allow either (implicit or explicit) syntax but enforce consistency within a single hash
- consistent_either
# Force hashes that have a symbol value to use hash rockets
UseHashRocketsWithSymbolValues: false
# Do not suggest { a?: 1 } over { :a? => 1 } in ruby19 style
Expand Down
11 changes: 9 additions & 2 deletions lib/rubocop/cop/mixin/hash_shorthand_syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ def register_offense(node, message, replacement) # rubocop:disable Metrics/AbcSi
end

def ignore_mixed_hash_shorthand_syntax?(hash_node)
target_ruby_version <= 3.0 || enforced_shorthand_syntax != 'consistent' ||
target_ruby_version <= 3.0 ||
!%w[consistent consistent_either].include?(enforced_shorthand_syntax) ||
!hash_node.hash_type?
end

def ignore_hash_shorthand_syntax?(pair_node)
target_ruby_version <= 3.0 || enforced_shorthand_syntax == 'either' ||
enforced_shorthand_syntax == 'consistent' ||
%w[consistent consistent_either].include?(enforced_shorthand_syntax) ||
!pair_node.parent.hash_type?
end

Expand Down Expand Up @@ -172,6 +173,11 @@ def hash_with_values_that_cant_be_omitted?(hash_value_type_breakdown)
hash_value_type_breakdown[:value_needed]&.any?
end

def ignore_explicit_ommitable_hash_shorthand_syntax?(hash_value_type_breakdown)
hash_value_type_breakdown.keys == [:value_omittable] &&
enforced_shorthand_syntax == 'consistent_either'
end

def each_omitted_value_pair(hash_value_type_breakdown, &block)
hash_value_type_breakdown[:value_omitted]&.each(&block)
end
Expand All @@ -198,6 +204,7 @@ def mixed_shorthand_syntax_check(hash_value_type_breakdown)

def no_mixed_shorthand_syntax_check(hash_value_type_breakdown)
return if hash_with_values_that_cant_be_omitted?(hash_value_type_breakdown)
return if ignore_explicit_ommitable_hash_shorthand_syntax?(hash_value_type_breakdown)

each_omittable_value_pair(hash_value_type_breakdown) do |pair_node|
hash_key_source = pair_node.key.source
Expand Down
18 changes: 18 additions & 0 deletions lib/rubocop/cop/style/hash_syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module Style
# * never - forces use of explicit hash literal value
# * either - accepts both shorthand and explicit use of hash literal value
# * consistent - forces use of the 3.1 syntax only if all values can be omitted in the hash
# * consistent_either - accepts both shorthand and explicit use of hash literal value,
# but they must be consistent
#
# @example EnforcedStyle: ruby19 (default)
# # bad
Expand Down Expand Up @@ -110,6 +112,22 @@ module Style
# # good - can't omit `baz`
# {foo: foo, bar: baz}
#
# @example EnforcedShorthandSyntax: consistent_either
#
# # good - `foo` and `bar` values can be omitted, but they are consistent, so it's accepted
# {foo: foo, bar: bar}
#
# # bad - `bar` value can be omitted
# {foo:, bar: bar}
#
# # bad - mixed syntaxes
# {foo:, bar: baz}
#
# # good
# {foo:, bar:}
#
# # good - can't omit `baz`
# {foo: foo, bar: baz}
class HashSyntax < Base
include ConfigurableEnforcedStyle
include HashShorthandSyntax
Expand Down
10 changes: 5 additions & 5 deletions spec/rubocop/cli/auto_gen_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1500,7 +1500,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
.to eq(<<~YAML)
# Configuration parameters: EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
Style/HashSyntax:
EnforcedStyle: hash_rockets
YAML
Expand All @@ -1514,7 +1514,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
.to eq(<<~YAML)
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
Style/HashSyntax:
Exclude:
- 'example1.rb'
Expand All @@ -1531,7 +1531,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
.to eq(<<~YAML)
# Configuration parameters: EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
Style/HashSyntax:
Exclude:
- 'example1.rb'
Expand All @@ -1546,7 +1546,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
.to eq(<<~YAML)
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
Style/HashSyntax:
Exclude:
- 'example1.rb'
Expand Down Expand Up @@ -1583,7 +1583,7 @@ def function(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
.to eq(<<~YAML)
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
# SupportedShorthandSyntax: always, never, either, consistent
# SupportedShorthandSyntax: always, never, either, consistent, consistent_either
Style/HashSyntax:
Exclude:
- 'example1.rb'
Expand Down
68 changes: 67 additions & 1 deletion spec/rubocop/cop/style/hash_syntax_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,7 @@ def buz(foo:, bar:); end
end
end

context 'configured to disallow mixing of implicit and explicit hash literal value' do
context 'configured to disallow mixing of implicit and explicit hash literal value, but prefers shorthand syntax whenever possible' do
let(:cop_config) do
{
'EnforcedStyle' => 'ruby19',
Expand Down Expand Up @@ -1677,4 +1677,70 @@ def buz(foo:, bar:); end
end
end
end

context 'configured to disallow mixing of implicit and explicit hash literal value' do
let(:cop_config) do
{
'EnforcedStyle' => 'ruby19',
'SupportedStyles' => %w[ruby19 hash_rockets],
'EnforcedShorthandSyntax' => 'consistent_either'
}
end

context 'Ruby >= 3.1', :ruby31 do
it 'does not register an offense when all hash values are omitted' do
expect_no_offenses(<<~RUBY)
{foo:, bar:}
RUBY
end

it 'registers an offense when some hash values are omitted but they can all be omitted' do
expect_offense(<<~RUBY)
{foo:, bar: bar}
^^^ Do not mix explicit and implicit hash values. Omit the hash value.
RUBY

expect_correction(<<~RUBY)
{foo:, bar:}
RUBY
end

it 'registers an offense when some hash values are omitted but they cannot all be omitted' do
expect_offense(<<~RUBY)
{foo:, bar: baz}
^^^ Do not mix explicit and implicit hash values. Include the hash value.
RUBY

expect_correction(<<~RUBY)
{foo: foo, bar: baz}
RUBY
end

it 'does not register an offense when all hash values are present, but no values can be omitted' do
expect_no_offenses(<<~RUBY)
{foo: bar, bar: foo}
RUBY
end

it 'does not register an offense when all hash values are present, but only some values can be omitted' do
expect_no_offenses(<<~RUBY)
{foo: baz, bar: bar}
RUBY
end

it 'does not register an offense when all hash values are present, but can all be omitted' do
expect_no_offenses(<<~RUBY)
{foo: foo, bar: bar}
RUBY
end
end

context 'Ruby <= 3.0', :ruby30, unsupported_on: :prism do
it 'does not register an offense when hash key and hash value are the same' do
expect_no_offenses(<<~RUBY)
{foo: foo, bar: bar}
RUBY
end
end
end
end

0 comments on commit 71abb5d

Please sign in to comment.