Skip to content

Commit

Permalink
[Fix rubocop#1608] Add 'align_braces' style for Style/IndentHash
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdowad committed Nov 6, 2015
1 parent 1da2583 commit 0931410
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* [#2173](https://github.com/bbatsov/rubocop/issues/2173): `Style/AlignParameters` also checks parameter alignment for method definitions. ([@alexdowad][])
* [#1825](https://github.com/bbatsov/rubocop/issues/1825): New `NameWhitelist` configuration parameter for `Style/PredicateName` can be used to suppress errors on known-good predicate names. ([@alexdowad][])
* `Style/Documentation` recognizes 'Constant = Class.new' as a class definition. ([@alexdowad][])
* [#1608](https://github.com/bbatsov/rubocop/issues/1608): Add new 'align_braces' style for `Style/IndentHash`. ([@alexdowad][])

### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ Style/IndentHash:
SupportedStyles:
- special_inside_parentheses
- consistent
- align_braces

Style/LambdaCall:
EnforcedStyle: call
Expand Down
104 changes: 67 additions & 37 deletions lib/rubocop/cop/style/indent_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,37 @@ module Style
# 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.
# By default, 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.
#
# This default style is called 'special_inside_parentheses'. Alternative
# styles are 'consistent' and 'align_braces'. Here are examples:
#
# # special_inside_parentheses
# hash = {
# key: :value
# }
# but_in_a_method_call({
# its_like: :this
# })
# # consistent
# hash = {
# key: :value
# }
# and_in_a_method_call({
# no: :difference
# })
# # align_braces
# and_now_for_something = {
# completely: :different
# }
#
class IndentHash < Cop
include AutocorrectAlignment
include ConfigurableEnforcedStyle
Expand Down Expand Up @@ -62,13 +85,16 @@ def check(hash_node, left_brace, left_parenthesis)
end

def check_right_brace(right_brace, left_brace, left_parenthesis)
# if the right brace is on the same line as the last value, accept
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
msg = if style == :align_braces
'Indent the right brace the same as the left brace.'
elsif style == :special_inside_parentheses && left_parenthesis
'Indent the right brace the same as the first position ' \
'after the preceding left parenthesis.'
else
Expand All @@ -93,15 +119,24 @@ def check_based_on_longest_key(pairs, left_brace, left_parenthesis)
end

def check_first_pair(first_pair, left_brace, left_parenthesis, offset)
column = first_pair.loc.expression.column
actual_column = first_pair.loc.expression.column
expected_column = base_column(left_brace, left_parenthesis) +
configured_indentation_width + offset
@column_delta = expected_column - column
@column_delta = expected_column - actual_column

if @column_delta == 0
correct_style_detected
# which column was actually used as 'base column' for indentation?
# (not the column which we think should be the 'base column',
# but the one which has actually been used for that purpose)
base_column = actual_column - configured_indentation_width - offset
styles = detected_styles(base_column, left_parenthesis, left_brace)
if styles.size > 1
ambiguous_style_detected(*styles)
else
correct_style_detected
end
else
incorrect_style_detected(column, offset, first_pair,
incorrect_style_detected(actual_column, offset, first_pair,
left_parenthesis, left_brace)
end
end
Expand All @@ -110,17 +145,29 @@ 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
base_column = column - configured_indentation_width - offset
styles = detected_styles(base_column, left_parenthesis, left_brace)
ambiguous_style_detected(*styles)
end
end

def detected_styles(column, left_parenthesis, left_brace)
styles = []
if column == (left_brace.source_line =~ /\S/)
styles << :consistent
styles << :special_inside_parentheses unless left_parenthesis
end
if left_parenthesis && column == left_parenthesis.column + 1
styles << :special_inside_parentheses
end
styles << :align_braces if column == left_brace.column
styles
end

def base_column(left_brace, left_parenthesis)
if left_parenthesis && style == :special_inside_parentheses
if style == :align_braces
left_brace.column
elsif left_parenthesis && style == :special_inside_parentheses
left_parenthesis.column + 1
else
left_brace.source_line =~ /\S/
Expand All @@ -129,32 +176,15 @@ def base_column(left_brace, left_parenthesis)

# Returns the description of what the correct indentation is based on.
def base_description(left_parenthesis)
if left_parenthesis && style == :special_inside_parentheses
if style == :align_braces
'the position of the opening brace'
elsif 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)
Expand Down
115 changes: 111 additions & 4 deletions spec/rubocop/cop/style/indent_hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
subject(:cop) { described_class.new(config) }
let(:config) do
supported_styles = {
'SupportedStyles' => %w(special_inside_parentheses consistent)
'SupportedStyles' => %w(special_inside_parentheses consistent
align_braces)
}
RuboCop::Config.new('Style/AlignHash' => align_hash_config,
'Style/IndentHash' =>
Expand Down Expand Up @@ -239,21 +240,35 @@
expect(cop.offenses).to be_empty
end

it 'registers an offense for incorrect indentation' do
it "registers an offense for 'consistent' indentation" do
inspect_source(cop,
['func({',
' a: 1',
'})'])
expect(cop.messages)
.to eq(['Use 2 spaces for indentation in a hash, relative to the' \
' first position after the preceding left parenthesis.',

'Indent the right brace the same as the first position ' \
'after the preceding left parenthesis.'])
expect(cop.config_to_allow_offenses)
.to eq('EnforcedStyle' => 'consistent')
end

it "registers an offense for 'align_braces' indentation" do
inspect_source(cop,
['var = {',
' a: 1',
' }'])
# since there are no parens, warning message is for 'consistent' style
expect(cop.messages)
.to eq(['Use 2 spaces for indentation in a hash, relative to the' \
' start of the line where the left curly brace is.',
'Indent the right brace the same as the start of the ' \
'line where the left brace is.'])
expect(cop.config_to_allow_offenses)
.to eq('EnforcedStyle' => 'align_braces')
end

it 'auto-corrects incorrectly indented first pair' do
corrected = autocorrect_source(cop, ['func({',
' a: 1',
Expand Down Expand Up @@ -319,7 +334,8 @@
'Indent the right brace the same as the start of the ' \
'line where the left brace is.'])
expect(cop.config_to_allow_offenses)
.to eq('EnforcedStyle' => 'special_inside_parentheses')
.to eq('EnforcedStyle' => %w(special_inside_parentheses
align_braces))
end

it 'accepts normal indentation for second argument' do
Expand Down Expand Up @@ -364,4 +380,95 @@
end
end
end

context 'when EnforcedStyle is align_braces' do
let(:cop_config) { { 'EnforcedStyle' => 'align_braces' } }

it 'accepts correctly indented first pair' do
inspect_source(cop,
['a = {',
' a: 1',
' }'])
expect(cop.offenses).to be_empty
end

it 'accepts several pairs per line' do
inspect_source(cop,
['a = {',
' a: 1, b: 2',
' }'])
expect(cop.offenses).to be_empty
end

it 'accepts a first pair on the same line as the left brace' do
inspect_source(cop,
['a = { "a" => 1,',
' "b" => 2 }'])
expect(cop.offenses).to be_empty
end

it 'accepts single line hash' do
inspect_source(cop,
'a = { a: 1, b: 2 }')
expect(cop.offenses).to be_empty
end

it 'accepts an empty hash' do
inspect_source(cop,
'a = {}')
expect(cop.offenses).to be_empty
end

context "when 'consistent' style is used" do
it 'registers an offense for incorrect indentation' do
inspect_source(cop,
['func({',
' a: 1',
'})'])
expect(cop.messages)
.to eq(['Use 2 spaces for indentation in a hash, relative to the' \
' position of the opening brace.',
'Indent the right brace the same as the left brace.'])
expect(cop.config_to_allow_offenses)
.to eq('EnforcedStyle' => 'consistent')
end

it 'auto-corrects incorrectly indented first pair' do
corrected = autocorrect_source(cop, ['var = {',
' a: 1',
'}'])
expect(corrected).to eq ['var = {',
' a: 1',
' }'].join("\n")
end
end

context "when 'special_inside_parentheses' style is used" do
it 'registers an offense for incorrect indentation' do
inspect_source(cop,
['var = {',
' a: 1',
'}',
'func({',
' a: 1',
' })'])
expect(cop.messages)
.to eq(['Use 2 spaces for indentation in a hash, relative to the' \
' position of the opening brace.',
'Indent the right brace the same as the left brace.'])
expect(cop.config_to_allow_offenses)
.to eq('EnforcedStyle' => 'special_inside_parentheses')
end
end

it 'registers an offense for incorrectly indented }' do
inspect_source(cop,
['a << {',
' }'])
expect(cop.highlights).to eq(['}'])
expect(cop.messages)
.to eq(['Indent the right brace the same as the left brace.'])
expect(cop.config_to_allow_offenses).to be_empty
end
end
end

0 comments on commit 0931410

Please sign in to comment.