lib/rubocop/cop/style/align_hash.rb in rubocop-0.14.1 vs lib/rubocop/cop/style/align_hash.rb in rubocop-0.15.0

- old
+ new

@@ -4,157 +4,222 @@ module Cop module Style # Here we check if the keys, separators, and values of a multi-line hash # literal are aligned. class AlignHash < Cop - MSG = 'Align the elements of a hash literal if they span more than ' + - 'one line.' + # Handles calculation of deltas (deviations from correct alignment) + # when the enforced style is 'key'. + class KeyAlignment + def checkable_layout(_node) + true + end - def on_hash(node) - first_pair = node.children.first + def deltas_for_first_pair(*_) + {} # The first pair is always considered correct. + end - styles = [cop_config['EnforcedHashRocketStyle'], - cop_config['EnforcedColonStyle']] + def deltas(first_pair, prev_pair, current_pair) + if current_pair.loc.line == prev_pair.loc.line + {} + else + { key: first_pair.loc.column - current_pair.loc.column } + end + end + end - if styles.include?('table') || styles.include?('separator') - return if any_pairs_on_the_same_line?(node) + # Common functionality for the styles where not only keys, but also + # values are aligned. + class AlignmentOfValues + def checkable_layout(node) + !any_pairs_on_the_same_line?(node) && all_have_same_sparator?(node) end - if styles.include?('table') + def deltas(first_pair, prev_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 any_pairs_on_the_same_line?(node) + lines_of_the_children = node.children.map do |pair| + key, _value = *pair + key.loc.line + end + lines_of_the_children.uniq.size < lines_of_the_children.size + end + + def all_have_same_sparator?(node) + first_separator = node.children.first.loc.operator.source + node.children[1..-1].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.loc.expression.source.length end @max_key_width = key_widths.max - if first_pair && value_delta(nil, first_pair, @max_key_width) != 0 - @column_deltas = {} - convention(first_pair, :expression) + + 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 separator_delta(first_pair, current_separator, key_delta) + if current_separator.is?(':') + 0 # Colon follows directly after key + else + first_pair.loc.column + @max_key_width + 1 - + current_separator.column - key_delta end end - node.children.each_cons(2) do |prev, current| - @column_deltas = deltas(first_pair, prev, current, @max_key_width) - convention(current, :expression) unless good_alignment? + 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 - end - def any_pairs_on_the_same_line?(node) - lines_of_the_children = node.children.map do |pair| - key, _value = *pair - key.loc.line + def spaced_separator(node) + node.loc.operator.is?('=>') ? ' => ' : ': ' end - lines_of_the_children.uniq.size < lines_of_the_children.size 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. Some local variables fix the problem. - max_key_width = @max_key_width - key_delta = @column_deltas[:key] || 0 + # Handles calculation of deltas when the enforced style is 'separator'. + class SeparatorAlignment < AlignmentOfValues + def deltas_for_first_pair(first_pair, node) + {} # The first pair is always considered correct. + end - key, value = *node + private - @corrections << lambda do |corrector| - expr = node.loc.expression - b = expr.begin_pos - b -= key_delta.abs if key_delta < 0 - range = Parser::Source::Range.new(expr.source_buffer, b, - expr.end_pos) - source = ' ' * [key_delta, 0].max + - if enforced_style(node) == 'key' - expr.source - else - key_source = key.loc.expression.source - padded_separator = case enforced_style(node) - when 'separator' - spaced_separator(node) - when 'table' - space = ' ' * (max_key_width - - key_source.length) - if node.loc.operator.is?('=>') - space + spaced_separator(node) - else - spaced_separator(node) + space - end - end - key_source + padded_separator + value.loc.expression.source - end - corrector.replace(range, source) + def key_delta(first_pair, current_pair) + key_end_column(first_pair) - key_end_column(current_pair) end - end - private + def key_end_column(pair) + key, _value = *pair + key.loc.column + key.loc.expression.source.length + end - def good_alignment? - @column_deltas.values.compact.none? { |v| v != 0 } + def separator_delta(first_pair, current_separator, key_delta) + if current_separator.is?(':') + 0 # Colon follows directly after key + else + first_pair.loc.operator.column - current_separator.column - + key_delta + end + 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 - def deltas(first_pair, prev_pair, current_pair, max_key_width) - enforced_style = enforced_style(current_pair) - unless %w(key separator table).include?(enforced_style) - fail "Unknown #{config_parameter(current_pair)}: #{enforced_style}" + MSG = 'Align the elements of a hash literal if they span more than ' + + 'one line.' + + def on_hash(node) + return if node.children.empty? + + @alignment_for_hash_rockets ||= + new_alignment('EnforcedHashRocketStyle') + @alignment_for_colons ||= new_alignment('EnforcedColonStyle') + + first_pair = node.children.first + + unless @alignment_for_hash_rockets.checkable_layout(node) && + @alignment_for_colons.checkable_layout(node) + return end - return {} if current_pair.loc.line == prev_pair.loc.line + @column_deltas = alignment_for(first_pair) + .deltas_for_first_pair(first_pair, node) + convention(first_pair, :expression) unless good_alignment? - key_left_alignment_delta = (first_pair.loc.column - - current_pair.loc.column) - if enforced_style == 'key' - { key: key_left_alignment_delta } - else - { - key: if enforced_style == 'table' - key_left_alignment_delta - else - key_end_column(first_pair) - key_end_column(current_pair) - end, - separator: if current_pair.loc.operator.is?(':') && - enforced_style == 'table' - # Colon follows directly after key - (key_end_column(current_pair) - - current_pair.loc.operator.column) - else - # Aligned separator - (first_pair.loc.operator.column - - current_pair.loc.operator.column) - end, - value: value_delta(first_pair, current_pair, max_key_width) - } + node.children.each_cons(2) do |prev, current| + @column_deltas = alignment_for(current).deltas(first_pair, prev, + current) + convention(current, :expression) unless good_alignment? end end - def key_end_column(pair) - key, _value = *pair - key.loc.column + key.loc.expression.source.length + private + + def alignment_for(pair) + if pair.loc.operator.is?('=>') + @alignment_for_hash_rockets + else + @alignment_for_colons + end end - def value_delta(first_pair, current_pair, max_key_width) - key, value = *current_pair - correct_value_column = if enforced_style(current_pair) == 'table' - key.loc.column + - spaced_separator(current_pair).length + - max_key_width - elsif first_pair.nil? # Only one pair? - value.loc.column - else - _key1, value1 = *first_pair - value1.loc.column - end - correct_value_column - value.loc.column + 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. Some local variables fix the problem. + key_delta = @column_deltas[:key] || 0 + separator_delta = @column_deltas[:separator] || 0 + value_delta = @column_deltas[:value] || 0 + + key, value = *node + + @corrections << lambda do |corrector| + adjust(corrector, key_delta, key.loc.expression) + adjust(corrector, separator_delta, node.loc.operator) + adjust(corrector, value_delta, value.loc.expression) + end end - def spaced_separator(node) - node.loc.operator.is?('=>') ? ' => ' : ': ' + def new_alignment(key) + case cop_config[key] + when 'key' then KeyAlignment.new + when 'table' then TableAlignment.new + when 'separator' then SeparatorAlignment.new + else fail "Unknown #{key}: #{cop_config[key]}" + end end - def enforced_style(node) - cop_config[config_parameter(node)] + def adjust(corrector, delta, range) + if delta > 0 + corrector.insert_before(range, ' ' * delta) + elsif delta < 0 + range = Parser::Source::Range.new(range.source_buffer, + range.begin_pos - delta.abs, + range.begin_pos) + corrector.remove(range) + end end - def config_parameter(node) - separator = node.loc.operator.is?('=>') ? 'HashRocket' : 'Colon' - "Enforced#{separator}Style" + def good_alignment? + @column_deltas.values.compact.none? { |v| v != 0 } end end end end end