123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Style::HashSyntax

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/style/hash_syntax.rb

Overview

Checks hash literal syntax.

It can enforce either the use of the class hash rocket syntax or the use of the newer Ruby 1.9 syntax (when applicable).

A separate offense is registered for each problematic pair.

The supported styles are:

  • ruby19 - forces use of the 1.9 syntax (e.g. {a: 1}) when hashes have all symbols for keys

  • hash_rockets - forces use of hash rockets for all hashes

  • no_mixed_keys - simply checks for hashes with mixed syntaxes

  • ruby19_no_mixed_keys - forces use of ruby 1.9 syntax and forbids mixed syntax hashes

This cop has EnforcedShorthandSyntax option. It can enforce either the use of the explicit hash value syntax or the use of Ruby 3.1’s hash value shorthand syntax.

The supported styles are:

  • always - forces use of the 3.1 syntax (e.g. foo:)

  • 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

  • either_consistent - accepts both shorthand and explicit use of hash literal value, but they must be consistent

Examples:

EnforcedStyle: ruby19 (default)

# bad
{:a => 2}
{b: 1, :c => 2}

# good
{a: 2, b: 1}
{:c => 2, 'd' => 2} # acceptable since 'd' isn't a symbol
{d: 1, 'e' => 2} # technically not forbidden

EnforcedStyle: hash_rockets

# bad
{a: 1, b: 2}
{c: 1, 'd' => 5}

# good
{:a => 1, :b => 2}

EnforcedStyle: no_mixed_keys

# bad
{:a => 1, b: 2}
{c: 1, 'd' => 2}

# good
{:a => 1, :b => 2}
{c: 1, d: 2}

EnforcedStyle: ruby19_no_mixed_keys

# bad
{:a => 1, :b => 2}
{c: 2, 'd' => 3} # should just use hash rockets

# good
{a: 1, b: 2}
{:c => 3, 'd' => 4}

EnforcedShorthandSyntax: always (default)

# bad
{foo: foo, bar: bar}

# good
{foo:, bar:}

EnforcedShorthandSyntax: never

# bad
{foo:, bar:}

# good
{foo: foo, bar: bar}

EnforcedShorthandSyntax: either

# good
{foo: foo, bar: bar}

# good
{foo: foo, bar:}

# good
{foo:, bar:}

EnforcedShorthandSyntax: consistent

# bad - `foo` and `bar` values can be omitted
{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}

EnforcedShorthandSyntax: either_consistent

# 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}

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

SYMBOL_TO_STRING_CACHE

::RuboCop::Cop::HashShorthandSyntax - Included

DO_NOT_MIX_EXPLICIT_VALUE_MSG, DO_NOT_MIX_MSG_PREFIX, DO_NOT_MIX_OMIT_VALUE_MSG, EXPLICIT_HASH_VALUE_MSG, OMIT_HASH_VALUE_MSG

::RuboCop::Cop::RangeHelp - Included

BYTE_ORDER_MARK, NOT_GIVEN

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns an url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::RangeHelp - Included

#add_range, #column_offset_between,
#contents_range

A range containing only the contents of a literal with delimiters (e.g.

#directions,
#effective_column

Returns the column attribute of the range, except if the range is on the first line and there’s a byte order mark at the beginning of that line, in which case 1 is subtracted from the column value.

#final_pos, #move_pos, #move_pos_str, #range_between, #range_by_whole_lines, #range_with_comments, #range_with_comments_and_lines, #range_with_surrounding_comma, #range_with_surrounding_space, #source_range

::RuboCop::Cop::HashShorthandSyntax - Included

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?, #target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#acceptable_19_syntax_symbol?(sym_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 217

def acceptable_19_syntax_symbol?(sym_name)
  sym_name.delete_prefix!(':')

  if cop_config['PreferHashRocketsForNonAlnumEndingSymbols'] &&
     # Prefer { :production? => false } over { production?: false } and
     # similarly for other non-alnum final characters (except quotes,
     # to prefer { "x y": 1 } over { :"x y" => 1 }).
     !/[\p{Alnum}"']\z/.match?(sym_name)
    return false
  end

  # Most hash keys can be matched against a simple regex.
  return true if /\A[_a-z]\w*[?!]?\z/i.match?(sym_name)

  return false if target_ruby_version <= 2.1

  (sym_name.start_with?("'") && sym_name.end_with?("'")) ||
    (sym_name.start_with?('"') && sym_name.end_with?('"'))
end

#alternative_style

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 185

def alternative_style
  case style
  when :hash_rockets
    :ruby19
  when :ruby19, :ruby19_no_mixed_keys
    :hash_rockets
  end
end

#argument_without_space?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 274

def argument_without_space?(node)
  node.argument? && node.source_range.begin_pos == node.parent.loc.selector.end_pos
end

#autocorrect(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 196

def autocorrect(corrector, node)
  if style == :hash_rockets || force_hash_rockets?(node.parent.pairs)
    autocorrect_hash_rockets(corrector, node)
  elsif style == :ruby19_no_mixed_keys || style == :no_mixed_keys
    autocorrect_no_mixed_keys(corrector, node)
  else
    autocorrect_ruby19(corrector, node)
  end
end

#autocorrect_hash_rockets(corrector, pair_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 278

def autocorrect_hash_rockets(corrector, pair_node)
  op = pair_node.loc.operator

  key_with_hash_rocket = ":#{pair_node.key.source}#{pair_node.inverse_delimiter(true)}"
  key_with_hash_rocket += pair_node.key.source if pair_node.value_omission?
  corrector.replace(pair_node.key, key_with_hash_rocket)
  corrector.remove(range_with_surrounding_space(op))
end

#autocorrect_no_mixed_keys(corrector, pair_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 287

def autocorrect_no_mixed_keys(corrector, pair_node)
  if pair_node.colon?
    autocorrect_hash_rockets(corrector, pair_node)
  else
    autocorrect_ruby19(corrector, pair_node)
  end
end

#autocorrect_ruby19(corrector, pair_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 253

def autocorrect_ruby19(corrector, pair_node)
  range = range_for_autocorrect_ruby19(pair_node)

  space = argument_without_space?(pair_node.parent) ? ' ' : ''

  corrector.replace(range, range.source.sub(/^:(.*\S)\s*=>\s*$/, "#{space}\\1: "))

  hash_node = pair_node.parent
  return unless hash_node.parent&.return_type? && !hash_node.braces?

  corrector.wrap(hash_node, '{', '}')
end

#check(pairs, delim, msg) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 238

def check(pairs, delim, msg)
  pairs.each do |pair|
    if pair.delimiter == delim
      location = pair.source_range.begin.join(pair.loc.operator)
      add_offense(location, message: msg) do |corrector|
        autocorrect(corrector, pair)

        opposite_style_detected
      end
    else
      correct_style_detected
    end
  end
end

#force_hash_rockets?(pairs) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 295

def force_hash_rockets?(pairs)
  cop_config['UseHashRocketsWithSymbolValues'] && pairs.map(&:value).any?(&:sym_type?)
end

#hash_rockets_check(pairs)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 163

def hash_rockets_check(pairs)
  check(pairs, ':', MSG_HASH_ROCKETS)
end

#no_mixed_keys_check(pairs)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 177

def no_mixed_keys_check(pairs)
  if sym_indices?(pairs)
    check(pairs, pairs.first.inverse_delimiter, MSG_NO_MIXED_KEYS)
  else
    check(pairs, ':', MSG_NO_MIXED_KEYS)
  end
end

#on_hash(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 141

def on_hash(node)
  pairs = node.pairs

  return if pairs.empty?

  on_hash_for_mixed_shorthand(node)

  if style == :hash_rockets || force_hash_rockets?(pairs)
    hash_rockets_check(pairs)
  elsif style == :ruby19_no_mixed_keys
    ruby19_no_mixed_keys_check(pairs)
  elsif style == :no_mixed_keys
    no_mixed_keys_check(pairs)
  else
    ruby19_check(pairs)
  end
end

#range_for_autocorrect_ruby19(pair_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 266

def range_for_autocorrect_ruby19(pair_node)
  key = pair_node.key.source_range
  operator = pair_node.loc.operator

  range = key.join(operator)
  range_with_surrounding_space(range, side: :right)
end

#ruby19_check(pairs)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 159

def ruby19_check(pairs)
  check(pairs, '=>', MSG_19) if sym_indices?(pairs)
end

#ruby19_no_mixed_keys_check(pairs)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 167

def ruby19_no_mixed_keys_check(pairs)
  if force_hash_rockets?(pairs)
    check(pairs, ':', MSG_HASH_ROCKETS)
  elsif sym_indices?(pairs)
    check(pairs, '=>', MSG_19)
  else
    check(pairs, ':', MSG_NO_MIXED_KEYS)
  end
end

#sym_indices?(pairs) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 206

def sym_indices?(pairs)
  pairs.all? { |p| word_symbol_pair?(p) }
end

#word_symbol_pair?(pair) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/hash_syntax.rb', line 210

def word_symbol_pair?(pair)
  return false unless pair.key.sym_type? || pair.key.dsym_type?

  acceptable_19_syntax_symbol?(pair.key.source)
end