lib/rubocop/cop/style/align_hash.rb in rubocop-0.46.0 vs lib/rubocop/cop/style/align_hash.rb in rubocop-0.47.0
- old
+ new
@@ -1,221 +1,190 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Style
- # Here we check if the keys, separators, and values of a multi-line hash
- # literal are aligned.
+ # 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)
+ # - 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)
+ # - ignore_explicit (with curly braces)
+ #
+ # @example
+ #
+ # # EnforcedHashRocketStyle: key (default)
+ # # EnforcedColonStyle: key (default)
+ #
+ # # good
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ #
+ # # bad
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ #
+ # @example
+ #
+ # # EnforcedHashRocketStyle: separator
+ # # EnforcedColonStyle: separator
+ #
+ # #good
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ #
+ # #bad
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ #
+ # @example
+ #
+ # # EnforcedHashRocketStyle: table
+ # # EnforcedColonStyle: table
+ #
+ # #good
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
+ #
+ # #bad
+ # {
+ # foo: bar,
+ # ba: baz
+ # }
+ # {
+ # :foo => bar,
+ # :ba => baz
+ # }
class AlignHash < Cop
- # Handles calculation of deltas (deviations from correct alignment)
- # when the enforced style is 'key'.
- class KeyAlignment
- def checkable_layout(_node)
- true
- end
+ include HashAlignment
- def deltas_for_first_pair(*)
- {} # The first pair is always considered correct.
- end
-
- def deltas(first_pair, current_pair)
- if Util.begins_its_line?(current_pair.source_range)
- { key: first_pair.loc.column - current_pair.loc.column }
- else
- {}
- end
- end
- end
-
- # Common functionality for the styles where not only keys, but also
- # values are aligned.
- class AlignmentOfValues
- include HashNode # any_pairs_on_the_same_line?
-
- def checkable_layout(node)
- !any_pairs_on_the_same_line?(node) && all_have_same_separator?(node)
- end
-
- def deltas(first_pair, current_pair)
- key_delta = key_delta(first_pair, current_pair)
- current_separator = current_pair.loc.operator
- separator_delta = separator_delta(first_pair, current_separator,
- key_delta)
- value_delta = value_delta(first_pair, current_pair) -
- key_delta - separator_delta
-
- { key: key_delta, separator: separator_delta, value: value_delta }
- end
-
- private
-
- def separator_delta(first_pair, current_separator, key_delta)
- if current_separator.is?(':')
- 0 # Colon follows directly after key
- else
- hash_rocket_delta(first_pair, current_separator) - key_delta
- end
- end
-
- def all_have_same_separator?(node)
- first_separator = node.children.first.loc.operator.source
- node.children.butfirst.all? do |pair|
- pair.loc.operator.is?(first_separator)
- end
- end
- end
-
- # Handles calculation of deltas when the enforced style is 'table'.
- class TableAlignment < AlignmentOfValues
- # The table style is the only one where the first key-value pair can
- # be considered to have bad alignment.
- def deltas_for_first_pair(first_pair, node)
- key_widths = node.children.map do |pair|
- key, _value = *pair
- key.source.length
- end
- @max_key_width = key_widths.max
-
- separator_delta = separator_delta(first_pair,
- first_pair.loc.operator, 0)
- {
- separator: separator_delta,
- value: value_delta(first_pair, first_pair) - separator_delta
- }
- end
-
- private
-
- def key_delta(first_pair, current_pair)
- first_pair.loc.column - current_pair.loc.column
- end
-
- def hash_rocket_delta(first_pair, current_separator)
- first_pair.loc.column + @max_key_width + 1 -
- current_separator.column
- end
-
- def value_delta(first_pair, current_pair)
- first_key, = *first_pair
- _, current_value = *current_pair
- correct_value_column = first_key.loc.column +
- spaced_separator(current_pair).length +
- @max_key_width
- correct_value_column - current_value.loc.column
- end
-
- def spaced_separator(node)
- node.loc.operator.is?('=>') ? ' => ' : ': '
- end
- end
-
- # Handles calculation of deltas when the enforced style is 'separator'.
- class SeparatorAlignment < AlignmentOfValues
- def deltas_for_first_pair(*)
- {} # The first pair is always considered correct.
- end
-
- private
-
- def key_delta(first_pair, current_pair)
- key_end_column(first_pair) - key_end_column(current_pair)
- end
-
- def key_end_column(pair)
- key, _value = *pair
- key.loc.column + key.source.length
- end
-
- def hash_rocket_delta(first_pair, current_separator)
- first_pair.loc.operator.column - current_separator.column
- end
-
- def value_delta(first_pair, current_pair)
- _, first_value = *first_pair
- _, current_value = *current_pair
- first_value.loc.column - current_value.loc.column
- end
- end
-
MSG = 'Align the elements of a hash literal if they span more than ' \
'one line.'.freeze
def on_send(node)
- return unless (last_child = node.children.last) &&
- hash?(last_child) &&
- ignore_last_argument_hash?(last_child)
+ return if double_splat?(node)
- ignore_node(last_child)
+ last_argument = node.children.last
+
+ return unless last_argument.hash_type? &&
+ ignore_hash_argument?(last_argument)
+
+ ignore_node(last_argument)
end
def on_hash(node)
return if ignored_node?(node)
- return if node.children.empty?
- return unless node.multiline?
+ return if node.pairs.empty? || node.single_line?
- @alignment_for_hash_rockets ||=
- new_alignment('EnforcedHashRocketStyle')
- @alignment_for_colons ||= new_alignment('EnforcedColonStyle')
+ return unless alignment_for_hash_rockets.checkable_layout?(node) &&
+ alignment_for_colons.checkable_layout?(node)
- unless @alignment_for_hash_rockets.checkable_layout(node) &&
- @alignment_for_colons.checkable_layout(node)
- return
- end
-
check_pairs(node)
end
private
+ attr_accessor :column_deltas
+
+ def double_splat?(node)
+ node.children.last.is_a?(Symbol)
+ end
+
def check_pairs(node)
- first_pair = node.children.first
- @column_deltas = alignment_for(first_pair)
- .deltas_for_first_pair(first_pair, node)
+ first_pair = node.pairs.first
+ self.column_deltas = alignment_for(first_pair)
+ .deltas_for_first_pair(first_pair, node)
add_offense(first_pair, :expression) unless good_alignment?
node.children.each do |current|
- @column_deltas = alignment_for(current).deltas(first_pair, current)
+ self.column_deltas = alignment_for(current)
+ .deltas(first_pair, current)
add_offense(current, :expression) unless good_alignment?
end
end
- def ignore_last_argument_hash?(node)
+ def ignore_hash_argument?(node)
case cop_config['EnforcedLastArgumentHashStyle']
when 'always_inspect' then false
when 'always_ignore' then true
- when 'ignore_explicit' then explicit_hash?(node)
- when 'ignore_implicit' then !explicit_hash?(node)
+ when 'ignore_explicit' then node.braces?
+ when 'ignore_implicit' then !node.braces?
end
end
- def hash?(node)
- node.respond_to?(:type) && node.hash_type?
+ def alignment_for(pair)
+ if pair.hash_rocket?
+ alignment_for_hash_rockets
+ else
+ alignment_for_colons
+ end
end
- def explicit_hash?(node)
- node.loc.begin
+ def alignment_for_hash_rockets
+ @alignment_for_hash_rockets ||=
+ new_alignment('EnforcedHashRocketStyle')
end
- def alignment_for(pair)
- if pair.loc.operator.is?('=>')
- @alignment_for_hash_rockets
- else
- @alignment_for_colons
- end
+ def alignment_for_colons
+ @alignment_for_colons ||=
+ new_alignment('EnforcedColonStyle')
end
def autocorrect(node)
# 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.
- key_delta = @column_deltas[:key] || 0
- key, value = *node
+ key_delta = column_deltas[:key] || 0
- if value.nil?
+ if !node.value
correct_no_value(key_delta, node.source_range)
else
- correct_key_value(key_delta, key.source_range, value.source_range,
+ correct_key_value(key_delta, node.key.source_range,
+ node.value.source_range,
node.loc.operator)
end
end
def correct_no_value(key_delta, key)
@@ -224,12 +193,12 @@
def correct_key_value(key_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 = @column_deltas[:separator] || 0
- value_delta = @column_deltas[:value] || 0
+ separator_delta = column_deltas[:separator] || 0
+ value_delta = column_deltas[:value] || 0
key_column = key.column
key_delta = -key_column if key_delta < -key_column
lambda do |corrector|
@@ -256,10 +225,10 @@
corrector.remove(range)
end
end
def good_alignment?
- @column_deltas.values.compact.all?(&:zero?)
+ column_deltas.values.all?(&:zero?)
end
end
end
end
end