diff --git a/config/default.yml b/config/default.yml index fdb75f407bc3..42a0a3dbb9c7 100644 --- a/config/default.yml +++ b/config/default.yml @@ -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 diff --git a/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb b/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb index b2c888d85bc4..597ed80694a9 100644 --- a/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +++ b/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/rubocop/cop/style/hash_syntax.rb b/lib/rubocop/cop/style/hash_syntax.rb index b8b8640ef3e4..93d5a5761bae 100644 --- a/lib/rubocop/cop/style/hash_syntax.rb +++ b/lib/rubocop/cop/style/hash_syntax.rb @@ -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 @@ -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 diff --git a/spec/rubocop/cli/auto_gen_config_spec.rb b/spec/rubocop/cli/auto_gen_config_spec.rb index 67658e7d7c3e..7ff190c46edf 100644 --- a/spec/rubocop/cli/auto_gen_config_spec.rb +++ b/spec/rubocop/cli/auto_gen_config_spec.rb @@ -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 @@ -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' @@ -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' @@ -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' @@ -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' diff --git a/spec/rubocop/cop/style/hash_syntax_spec.rb b/spec/rubocop/cop/style/hash_syntax_spec.rb index 5cc0832cee30..e4a3508a8960 100644 --- a/spec/rubocop/cop/style/hash_syntax_spec.rb +++ b/spec/rubocop/cop/style/hash_syntax_spec.rb @@ -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', @@ -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