RuboCop Module: Indent_Hash

So, Jason wants us to pick a random module out of RuboCop to check out and see what it does. I let IRB choose which RuboCop module to investigate.

It chose indent_hash.rb.

Here’s what IRB picked (next time, I get to pick):

Indent_hash.rb RuboCop Module
# encoding: utf-8
module RuboCop
module Cop
module Style
# This cops checks the indentation of the first key in a hash literal
# where the opening brace and the first key are on separate lines. The
# other keys' indentations are handled by the AlignHash cop.
#
# Hash literals that are arguments in a method call with parentheses, and
# where the opening curly brace of the hash is on the same line as the
# opening parenthesis of the method call, shall have their first key
# indented one step (two spaces) more than the position inside the
# opening parenthesis.
#
# Other hash literals shall have their first key indented one step more
# than the start of the line where the opening curly brace is.
class IndentHash < Cop
include AutocorrectAlignment
include ConfigurableEnforcedStyle
def on_hash(node)
left_brace = node.loc.begin
check(node, left_brace, nil) if left_brace
end
def on_send(node)
_receiver, _method_name, *args = *node
left_parenthesis = node.loc.begin
return unless left_parenthesis
args.each do |arg|
on_node(:hash, arg, :send) do |hash_node|
left_brace = hash_node.loc.begin
if left_brace && left_brace.line == left_parenthesis.line
check(hash_node, left_brace, left_parenthesis)
ignore_node(hash_node)
end
end
end
end
private
def check(hash_node, left_brace, left_parenthesis)
return if ignored_node?(hash_node)
first_pair = hash_node.children.first
if first_pair
left_brace = hash_node.loc.begin
return if first_pair.loc.expression.line == left_brace.line
if separator_style?(first_pair)
check_based_on_longest_key(hash_node.children, left_brace,
left_parenthesis)
else
check_first_pair(first_pair, left_brace, left_parenthesis, 0)
end
end
check_right_brace(hash_node.loc.end, left_brace, left_parenthesis)
end
def check_right_brace(right_brace, left_brace, left_parenthesis)
return if right_brace.source_line[0...right_brace.column] =~ /\S/
expected_column = base_column(left_brace, left_parenthesis)
@column_delta = expected_column - right_brace.column
return if @column_delta == 0
msg = if style == :special_inside_parentheses && left_parenthesis
'Indent the right brace the same as the first position ' \
'after the preceding left parenthesis.'
else
'Indent the right brace the same as the start of the line ' \
'where the left brace is.'
end
add_offense(right_brace, right_brace, msg)
end
def separator_style?(first_pair)
separator = first_pair.loc.operator
key = "Enforced#{separator.is?(':') ? 'Colon' : 'HashRocket'}Style"
config.for_cop('Style/AlignHash')[key] == 'separator'
end
def check_based_on_longest_key(pairs, left_brace, left_parenthesis)
key_lengths = pairs.map do |pair|
pair.children.first.loc.expression.length
end
check_first_pair(pairs.first, left_brace, left_parenthesis,
key_lengths.max - key_lengths.first)
end
def check_first_pair(first_pair, left_brace, left_parenthesis, offset)
column = first_pair.loc.expression.column
expected_column = base_column(left_brace, left_parenthesis) +
configured_indentation_width + offset
@column_delta = expected_column - column
if @column_delta == 0
correct_style_detected
else
incorrect_style_detected(column, offset, first_pair,
left_parenthesis, left_brace)
end
end
def incorrect_style_detected(column, offset, first_pair,
left_parenthesis, left_brace)
add_offense(first_pair, :expression,
message(base_description(left_parenthesis))) do
if column == unexpected_column(left_brace, left_parenthesis,
offset)
opposite_style_detected
else
unrecognized_style_detected
end
end
end
def base_column(left_brace, left_parenthesis)
if left_parenthesis && style == :special_inside_parentheses
left_parenthesis.column + 1
else
left_brace.source_line =~ /\S/
end
end
# Returns the description of what the correct indentation is based on.
def base_description(left_parenthesis)
if left_parenthesis && style == :special_inside_parentheses
'the first position after the preceding left parenthesis'
else
'the start of the line where the left curly brace is'
end
end
# Returns the "unexpected column", which is the column that would be
# correct if the configuration was changed.
def unexpected_column(left_brace, left_parenthesis, offset)
# Set a crazy value by default, indicating that there's no other
# configuration that can be chosen to make the used indentation
# accepted.
unexpected_base_column = -1000
if left_parenthesis
unexpected_base_column = if style == :special_inside_parentheses
left_brace.source_line =~ /\S/
else
left_parenthesis.column + 1
end
end
unexpected_base_column + configured_indentation_width + offset
end
def message(base_description)
format('Use %d spaces for indentation in a hash, relative to %s.',
configured_indentation_width, base_description)
end
end
end
end
end
view raw indent_hash.rb hosted with ❤ by GitHub

Under the comments, it says that it polices the indentation of a hash’s first key, if the first key of the hash is on a separate line. Let’s see if we can make it angry.


Part One: The Curly Braces

Apparently we want to get the message, Indent the right brace the same as the first position after the preceding left parenthesis. or, Indent the right brace the same as the start of the line where the left brace is.

Incorrect Right Brace
angry_hash = {
first_key: 'first',
second_key: 'second',
third_key: 'third'
}
puts angry_hash

There’s our first attempt. We’re going to ignore the style guide about a hash being an argument of a method call for starters. Maybe we can get the first offense.

RuboCop’s Response
19:37:40 - INFO - Inspecting Ruby code style of all files
Inspecting 3 files
..C

Offenses:

make_rubocop_angry.rb:5:2: C: Indent the right brace the same as 
the start of the line where the left brace is.
 }
 ^

3 files inspected, 1 offense detected

There we go. If you can’t see it, the closing right brace has an extra space in our program. Since the best way to keep our code consistent is to follow style guides, we should have that right brace match the same indentation as where we started the assignment of the hash. So, it should match the first letter of the variable it is assigned to. Like this:

Correct Right Brace
angry_hash = {
first_key: 'first',
second_key: 'second',
third_key: 'third'
}
puts angry_hash

That’s easy enough to remember. Most indentation follows the same format. But what about the other offense? We have to be defining a hash as a parameter to a method call. Well, that’s very specific. Here we go:

Incorrect Brace with Method
def angry_method(first_hash, second_hash)
puts first_hash
puts second_hash
end
angry_method({
first_key: 'first',
second_key: 'second',
third_key: 'third'
},
first_key: 'first',
second_key: 'second',
third_key: 'third'
)

Wow, that was difficult. But this isolates just the one offense that is specified in this cop. Here it is:

RuboCop’s Response
20:11:52 - INFO - Inspecting Ruby code style: make_rubocop_angry.rb
Inspecting 1 file
C

Offenses:

make_rubocop_angry.rb:10:1: C: Indent the right brace the same as 
the first position after the preceding left parenthesis.
},
^

1 file inspected, 1 offense detected

When I first tried, I made a method that only called one hash. RuboCop didn’t want me to use curly braces at all, calling them redundant. So, I needed a method that had two different hashes as arguments, so it would be confusing if I didn’t use braces. Then, I had to find the correct placement of the keys themselves within the hash, otherwise a different offense would be reported. As stated in the module’s code, that is handled by another cop. But this code correctly aggravates only the one we are looking at. Now, how do we solve this offense?

I tried a few positions, but RuboCop is basically saying that the curly braces need to line up vertically. So, to fix it, we just add spaces (or tabs, if it ends up being even) until it lines up.

The Correction
def angry_method(first_hash, second_hash)
puts first_hash
puts second_hash
end
angry_method({
first_key: 'first',
second_key: 'second',
third_key: 'third'
},
first_key: 'first',
second_key: 'second',
third_key: 'third'
)

There! Notice the braces are right on top of each other. This is a special style preference for when you have braces within the parameters of a method call. It won’t come up often, but when it does, make sure it looks nice.

Confusion?

Inspecting this correctly formatted program, you may notice that the keys of the first hash are not lined up in the same column as the keys of the second hash. What’s the deal? Well, the first hash needs to be sandwiched with curly braces. If we didn’t have them, Ruby wouldn’t know when we were done with the first hash. Since we closed the first hash, all the remaining keys that we define are assumed to be part of the second hash. The second hash doesn’t need curly braces. Since the keys of the second hash are not sandwiched, they get lined up in the same column as the curly braces of the first hash.


Part Two: The First Key

Now, for the second part of this cop, we want to get one of two messages (I almost missed these, since these messages are at the end of the module). This one: Use __ spaces for indentation in a hash, relative to the first position after the preceding left parenthesis. or this one: Use __ spaces for indentation in a hash, relative to the start of the line where the left curly brace is.

Well, lets try moving the keys around. As the comments in the module say, we should move all the keys together, to avoid getting additional offenses. Those offenses would be handled by a different cop. Since the difference between these two offense messages are the same as the curly brace offenses, and for the sake of time, we’ll focus on the hash inside the method call.

Incorrect Key Indentation
def angry_method(first_hash, second_hash)
puts first_hash
puts second_hash
end
angry_method({
first_key: 'first',
second_key: 'second',
third_key: 'third'
},
first_key: 'first',
second_key: 'second',
third_key: 'third'
)

Notice the difference from our correct one. Our keys in the first hash of the method call are only indented one space. That produces this RuboCop offense:

RuboCop’s Response
16:06:39 - INFO - Inspecting Ruby code style: make_rubocop_angry.rb
Inspecting 1 file
C

Offenses:

make_rubocop_angry.rb:7:3: C: Use 2 spaces for indentation in a hash,
relative to the first position after the preceding left parenthesis.
  first_key: 'first',
  ^^^^^^^^^^^^^^^^^^

1 file inspected, 1 offense detected

In order to correct this style mistake, where should the keys be located? The offense says to indent it two spaces from the column where the first position would be after the parenthesis. That means RuboCop wants the keys to be lined up, two spaces to the right of the column that the curly braces occupy. Check out the same correction code above.


How to disable this RuboCop module

Instead of fixing the style, you could just turn off this specific module. I don’t recommend it, because this is a nice Cop to have, since you can’t always remember where the keys should be. In your .rubocop.yml file in your parent directory, add this preference:

Style/IndentHash:
Enabled: False
view raw .rubocop.yml hosted with ❤ by GitHub

As an alternative, you can change the enforced style that RuboCop will follow. If you select EnforcedStyle: special_inside_parentheses, then the module will follow the special that we learned about: the keys are aligned relative to the first position after the left parenthesis, which usually means they are indented rather far into your screen. If you select EnforcedStyle: consistent, it will default to the same as if the hash was not within a method call: the keys are indented two spaces (if two spaces is your default indentation) from the beginning of the line that the opening brace is located.

As shown, you can also add both special_inside_parentheses and consistent to SupportedStyles: to include both styles in RuboCop. It won’t be offended, as long as you satisfy one of the styles.

.rubocop.yml File
EnforcedStyle: special_inside_parentheses
SupportedStyles:
- special_inside_parentheses
- consistent
view raw .rubocop.yml hosted with ❤ by GitHub
RuboCop Module: Indent_Hash

Leave a comment