123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Layout::HashAlignment

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/layout/hash_alignment.rb

Overview

Check that the keys, separators, and values of a multi-line hash literal are aligned according to configuration. The configuration options are:

  • key (left align keys, one space before hash rockets and values)

  • separator (align hash rockets and colons, right align keys)

  • table (left align keys, hash rockets, and values)

The treatment of hashes passed as the last argument to a method call can also be configured. The options are:

  • always_inspect

  • always_ignore

  • ignore_implicit (without curly braces)

Alternatively you can specify multiple allowed styles. That’s done by passing a list of styles to EnforcedStyles.

Examples:

EnforcedHashRocketStyle: key (default)

# bad
{
  :foo => bar,
   :ba => baz
}
{
  :foo => bar,
  :ba  => baz
}

# good
{
  :foo => bar,
  :ba => baz
}

EnforcedHashRocketStyle: separator

# bad
{
  :foo => bar,
  :ba => baz
}
{
  :foo => bar,
  :ba  => baz
}

# good
{
  :foo => bar,
   :ba => baz
}

EnforcedHashRocketStyle: table

# bad
{
  :foo => bar,
   :ba => baz
}

# good
{
  :foo => bar,
  :ba  => baz
}

EnforcedColonStyle: key (default)

# bad
{
  foo: bar,
   ba: baz
}
{
  foo: bar,
  ba:  baz
}

# good
{
  foo: bar,
  ba: baz
}

EnforcedColonStyle: separator

# bad
{
  foo: bar,
  ba: baz
}

# good
{
  foo: bar,
   ba: baz
}

EnforcedColonStyle: table

# bad
{
  foo: bar,
  ba: baz
}

# good
{
  foo: bar,
  ba:  baz
}

EnforcedLastArgumentHashStyle: always_inspect (default)

# Inspect both implicit and explicit hashes.

# bad
do_something(foo: 1,
  bar: 2)

# bad
do_something({foo: 1,
  bar: 2})

# good
do_something(foo: 1,
             bar: 2)

# good
do_something(
  foo: 1,
  bar: 2
)

# good
do_something({foo: 1,
              bar: 2})

# good
do_something({
  foo: 1,
  bar: 2
})

EnforcedLastArgumentHashStyle: always_ignore

# Ignore both implicit and explicit hashes.

# good
do_something(foo: 1,
  bar: 2)

# good
do_something({foo: 1,
  bar: 2})

EnforcedLastArgumentHashStyle: ignore_implicit

# Ignore only implicit hashes.

# bad
do_something({foo: 1,
  bar: 2})

# good
do_something(foo: 1,
  bar: 2)

EnforcedLastArgumentHashStyle: ignore_explicit

# Ignore only explicit hashes.

# bad
do_something(foo: 1,
  bar: 2)

# good
do_something({foo: 1,
  bar: 2})

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::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 a 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::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_gem_version

Returns a gems locked versions (i.e.

#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 Attribute Details

#column_deltas (rw)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 218

attr_accessor :offenses_by, :column_deltas

#enforce_first_argument_with_fixed_indentation?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 384

def enforce_first_argument_with_fixed_indentation?
  argument_alignment_config = config.for_enabled_cop('Layout/ArgumentAlignment')
  argument_alignment_config['EnforcedStyle'] == 'with_fixed_indentation'
end

#offenses_by (rw)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 218

attr_accessor :offenses_by, :column_deltas

Instance Method Details

#add_offenses (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 265

def add_offenses
  kwsplat_offenses = offenses_by.delete(KeywordSplatAlignment)
  register_offenses_with_format(kwsplat_offenses, KeywordSplatAlignment)

  format, offenses = offenses_by.min_by { |_, v| v.length }
  register_offenses_with_format(offenses, format)
end

#adjust(corrector, delta, range) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 371

def adjust(corrector, delta, range)
  if delta.positive?
    corrector.insert_before(range, ' ' * delta)
  elsif delta.negative?
    range = range_between(range.begin_pos - delta.abs, range.begin_pos)
    corrector.remove(range)
  end
end

#alignment_for(pair) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 300

def alignment_for(pair)
  if pair.kwsplat_type?
    [KeywordSplatAlignment.new]
  elsif pair.hash_rocket?
    alignment_for_hash_rockets
  else
    alignment_for_colons
  end
end

#alignment_for_colons (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 314

def alignment_for_colons
  @alignment_for_colons ||= new_alignment('EnforcedColonStyle')
end

#alignment_for_hash_rockets (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 310

def alignment_for_hash_rockets
  @alignment_for_hash_rockets ||= new_alignment('EnforcedHashRocketStyle')
end

#argument_before_hash(hash_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 233

def argument_before_hash(hash_node)
  hash_node.left_sibling.respond_to?(:loc) ? hash_node.left_sibling : nil
end

#autocorrect_incompatible_with_other_cops?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 222

def autocorrect_incompatible_with_other_cops?(node)
  return false unless enforce_first_argument_with_fixed_indentation? &&
                      node.pairs.any? &&
                      node.parent&.call_type?

  left_sibling = argument_before_hash(node)
  parent_loc = node.parent.loc
  selector = left_sibling || parent_loc.selector || parent_loc.expression
  same_line?(selector, node.pairs.first)
end

#check_delta(delta, node:, alignment:) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 283

def check_delta(delta, node:, alignment:)
  offenses_by[alignment.class] ||= []
  return if good_alignment? delta

  column_deltas[alignment.class][node] = delta
  offenses_by[alignment.class].push(node)
end

#check_pairs(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 246

def check_pairs(node)
  first_pair = node.pairs.first
  reset!

  alignment_for(first_pair).each do |alignment|
    delta = alignment.deltas_for_first_pair(first_pair, node)
    check_delta delta, node: first_pair, alignment: alignment
  end

  node.children.each do |current|
    alignment_for(current).each do |alignment|
      delta = alignment.deltas(first_pair, current)
      check_delta delta, node: current, alignment: alignment
    end
  end

  add_offenses
end

#correct_key_value(corrector, delta, key, value, separator) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 337

def correct_key_value(corrector, delta, key, value, separator)
  # We can't use the instance variable inside the lambda. That would
  # just give each lambda the same reference and they would all get the
  # last value of each. Some local variables fix the problem.
  separator_delta = delta[:separator] || 0
  value_delta     = delta[:value]     || 0
  key_delta       = delta[:key]       || 0

  key_column = key.column
  key_delta = -key_column if key_delta < -key_column

  adjust(corrector, key_delta, key)
  adjust(corrector, separator_delta, separator)
  adjust(corrector, value_delta, value)
end

#correct_no_value(corrector, key_delta, key) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 333

def correct_no_value(corrector, key_delta, key)
  adjust(corrector, key_delta, key)
end

#correct_node(corrector, node, delta) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 318

def correct_node(corrector, node, delta)
  # We can't use the instance variable inside the lambda. That would
  # just give each lambda the same reference and they would all get the
  # last value of each. A local variable fixes the problem.

  if node.value && node.respond_to?(:value_omission?) && !node.value_omission?
    correct_key_value(corrector, delta, node.key.source_range,
                      node.value.source_range,
                      node.loc.operator)
  else
    delta_value = delta[:key] || 0
    correct_no_value(corrector, delta_value, node.source_range)
  end
end

#double_splat?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 242

def double_splat?(node)
  node.children.last.is_a?(Symbol)
end

#good_alignment?(column_deltas) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 380

def good_alignment?(column_deltas)
  column_deltas.values.all?(&:zero?)
end

#ignore_hash_argument?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 291

def ignore_hash_argument?(node)
  case cop_config['EnforcedLastArgumentHashStyle']
  when 'always_inspect'  then false
  when 'always_ignore'   then true
  when 'ignore_explicit' then node.braces?
  when 'ignore_implicit' then !node.braces?
  end
end

#new_alignment(key) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 353

def new_alignment(key)
  formats = cop_config[key]
  formats = [formats] if formats.is_a? String

  formats.uniq.map do |format|
    case format
    when 'key'
      KeyAlignment.new
    when 'table'
      TableAlignment.new
    when 'separator'
      SeparatorAlignment.new
    else
      raise "Unknown #{key}: #{formats}"
    end
  end
end

#on_hash(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 208

def on_hash(node)
  return if autocorrect_incompatible_with_other_cops?(node) || ignored_node?(node) ||
            node.pairs.empty? || node.single_line?

  proc = ->(a) { a.checkable_layout?(node) }
  return unless alignment_for_hash_rockets.any?(proc) && alignment_for_colons.any?(proc)

  check_pairs(node)
end

#on_send(node) Also known as: #on_super, #on_yield

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 195

def on_send(node)
  return if double_splat?(node)
  return unless node.arguments?

  last_argument = node.last_argument

  return unless last_argument.hash_type? && ignore_hash_argument?(last_argument)

  ignore_node(last_argument)
end

#on_super(node)

Alias for #on_send.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 205

alias on_super on_send

#on_yield(node)

Alias for #on_send.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 206

alias on_yield on_send

#register_offenses_with_format(offenses, format) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 273

def register_offenses_with_format(offenses, format)
  (offenses || []).each do |offense|
    add_offense(offense, message: MESSAGES[format]) do |corrector|
      delta = column_deltas[alignment_for(offense).first.class][offense]

      correct_node(corrector, offense, delta) unless delta.nil?
    end
  end
end

#reset! (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/hash_alignment.rb', line 237

def reset!
  self.offenses_by = {}
  self.column_deltas = Hash.new { |hash, key| hash[key] = {} }
end