# encoding: utf-8moduleRuboCopmoduleCopmoduleStyle# Here we check if the keys, separators, and values of a multi-line hash# literal are aligned.classAlignHash<CopincludeIgnoredNode# Handles calculation of deltas (deviations from correct alignment)# when the enforced style is 'key'.classKeyAlignmentdefcheckable_layout(_node)trueenddefdeltas_for_first_pair(*){}# The first pair is always considered correct.enddefdeltas(first_pair,prev_pair,current_pair)ifcurrent_pair.loc.line==prev_pair.loc.line{}else{key: first_pair.loc.column-current_pair.loc.column}endendend# Common functionality for the styles where not only keys, but also# values are aligned.classAlignmentOfValuesdefcheckable_layout(node)!any_pairs_on_the_same_line?(node)&&all_have_same_sparator?(node)enddefdeltas(first_pair,_prev_pair,current_pair)key_delta=key_delta(first_pair,current_pair)current_separator=current_pair.loc.operatorseparator_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}endprivatedefseparator_delta(first_pair,current_separator,key_delta)ifcurrent_separator.is?(':')0# Colon follows directly after keyelsehash_rocket_delta(first_pair,current_separator)-key_deltaendenddefany_pairs_on_the_same_line?(node)lines_of_the_children=node.children.mapdo|pair|key,_value=*pairkey.loc.lineendlines_of_the_children.uniq.size<lines_of_the_children.sizeenddefall_have_same_sparator?(node)first_separator=node.children.first.loc.operator.sourcenode.children[1..-1].all?do|pair|pair.loc.operator.is?(first_separator)endendend# Handles calculation of deltas when the enforced style is 'table'.classTableAlignment<AlignmentOfValues# The table style is the only one where the first key-value pair can# be considered to have bad alignment.defdeltas_for_first_pair(first_pair,node)key_widths=node.children.mapdo|pair|key,_value=*pairkey.loc.expression.source.lengthend@max_key_width=key_widths.maxseparator_delta=separator_delta(first_pair,first_pair.loc.operator,0){separator: separator_delta,value: value_delta(first_pair,first_pair)-separator_delta}endprivatedefkey_delta(first_pair,current_pair)first_pair.loc.column-current_pair.loc.columnenddefhash_rocket_delta(first_pair,current_separator)first_pair.loc.column+@max_key_width+1-current_separator.columnenddefvalue_delta(first_pair,current_pair)first_key,_=*first_pair_,current_value=*current_paircorrect_value_column=first_key.loc.column+spaced_separator(current_pair).length+@max_key_widthcorrect_value_column-current_value.loc.columnenddefspaced_separator(node)node.loc.operator.is?('=>')?' => ':': 'endend# Handles calculation of deltas when the enforced style is 'separator'.classSeparatorAlignment<AlignmentOfValuesdefdeltas_for_first_pair(*){}# The first pair is always considered correct.endprivatedefkey_delta(first_pair,current_pair)key_end_column(first_pair)-key_end_column(current_pair)enddefkey_end_column(pair)key,_value=*pairkey.loc.column+key.loc.expression.source.lengthenddefhash_rocket_delta(first_pair,current_separator)first_pair.loc.operator.column-current_separator.columnenddefvalue_delta(first_pair,current_pair)_,first_value=*first_pair_,current_value=*current_pairfirst_value.loc.column-current_value.loc.columnendendMSG='Align the elements of a hash literal if they span more than '\'one line.'defon_send(node)returnunless(last_child=node.children.last)&&hash?(last_child)&&ignore_last_argument_hash?(last_child)ignore_node(last_child)enddefon_hash(node)returnifignored_node?(node)returnifnode.children.empty?returnunlessmultiline?(node)@alignment_for_hash_rockets||=new_alignment('EnforcedHashRocketStyle')@alignment_for_colons||=new_alignment('EnforcedColonStyle')unless@alignment_for_hash_rockets.checkable_layout(node)&&@alignment_for_colons.checkable_layout(node)returnendcheck_pairs(node)endprivatedefcheck_pairs(node)first_pair=node.children.first@column_deltas=alignment_for(first_pair).deltas_for_first_pair(first_pair,node)add_offense(first_pair,:expression)unlessgood_alignment?node.children.each_cons(2)do|prev,current|@column_deltas=alignment_for(current).deltas(first_pair,prev,current)add_offense(current,:expression)unlessgood_alignment?endenddefignore_last_argument_hash?(node)casecop_config['EnforcedLastArgumentHashStyle']when'always_inspect'thenfalsewhen'always_ignore'thentruewhen'ignore_explicit'thenexplicit_hash?(node)when'ignore_implicit'then!explicit_hash?(node)endenddefhash?(node)node.respond_to?(:type)&&node.type==:hashenddefexplicit_hash?(node)node.loc.beginend# Returns true if the hash spans multiple linesdefmultiline?(node)returnfalseunlessnode.loc.expression.source.include?("\n")returnfalseifnode.children[1..-1].all?do|child|!begins_its_line?(child.loc.expression)endtrueenddefalignment_for(pair)ifpair.loc.operator.is?('=>')@alignment_for_hash_rocketselse@alignment_for_colonsendenddefautocorrect(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]||0separator_delta=@column_deltas[:separator]||0value_delta=@column_deltas[:value]||0key,value=*node@corrections<<lambdado|corrector|adjust(corrector,key_delta,key.loc.expression)adjust(corrector,separator_delta,node.loc.operator)adjust(corrector,value_delta,value.loc.expression)endenddefnew_alignment(key)casecop_config[key]when'key'thenKeyAlignment.newwhen'table'thenTableAlignment.newwhen'separator'thenSeparatorAlignment.newelsefail"Unknown #{key}: #{cop_config[key]}"endenddefadjust(corrector,delta,range)ifdelta>0corrector.insert_before(range,' '*delta)elsifdelta<0range=Parser::Source::Range.new(range.source_buffer,range.begin_pos-delta.abs,range.begin_pos)corrector.remove(range)endenddefgood_alignment?@column_deltas.values.compact.none?{|v|v!=0}endendendendend