# frozen_string_literal: truemoduleRuboCopmoduleCop# This mixin detects collections that are safe to "break"# by inserting new lines. This is useful for breaking# up long lines.## Let's look at hashes as an example:## We know hash keys are safe to break across lines. We can add# linebreaks into hashes on lines longer than the specified maximum.# Then in further passes cops can clean up the multi-line hash.# For example, say the maximum line length is as indicated below:## |# v# {foo: "0000000000", bar: "0000000000", baz: "0000000000"}## In a LineLength autocorrection pass, a line is added before# the first key that exceeds the column limit:## {foo: "0000000000", bar: "0000000000",# baz: "0000000000"}## In a MultilineHashKeyLineBreaks pass, lines are inserted# before all keys:## {foo: "0000000000",# bar: "0000000000",# baz: "0000000000"}## Then in future passes FirstHashElementLineBreak,# MultilineHashBraceLayout, and TrailingCommaInHashLiteral will# manipulate as well until we get:## {# foo: "0000000000",# bar: "0000000000",# baz: "0000000000",# }## (Note: Passes may not happen exactly in this sequence.)moduleCheckLineBreakabledefextract_breakable_node(node,max)ifnode.send_type?args=process_args(node.arguments)returnextract_breakable_node_from_elements(node,args,max)elsifnode.def_type?returnextract_breakable_node_from_elements(node,node.arguments,max)elsifnode.array_type?||node.hash_type?returnextract_breakable_node_from_elements(node,node.children,max)endnilendprivate# @api privatedefextract_breakable_node_from_elements(node,elements,max)returnunlessbreakable_collection?(node,elements)returnifsafe_to_ignore?(node)line=processed_source.lines[node.first_line-1]returnifprocessed_source.line_with_comment?(node.loc.line)returnifline.length<=maxextract_first_element_over_column_limit(node,elements,max)end# @api privatedefextract_first_element_over_column_limit(node,elements,max)line=node.first_line# If a `send` node is not parenthesized, don't move the first element, because it# can result in changed behavior or a syntax error.ifnode.send_type?&&!node.parenthesized?&&!first_argument_is_heredoc?(node)elements=elements.drop(1)endi=0i+=1whilewithin_column_limit?(elements[i],max,line)i=shift_elements_for_heredoc_arg(node,elements,i)returnifi.nil?returnelements.firstifi.zero?elements[i-1]end# @api privatedeffirst_argument_is_heredoc?(node)first_argument=node.first_argumentfirst_argument.respond_to?(:heredoc?)&&first_argument.heredoc?end# @api private# If a send node contains a heredoc argument, splitting cannot happen# after the heredoc or else it will cause a syntax error.defshift_elements_for_heredoc_arg(node,elements,index)returnindexunlessnode.send_type?||node.array_type?heredoc_index=elements.index{|arg|arg.respond_to?(:heredoc?)&&arg.heredoc?}returnindexunlessheredoc_indexreturnnilifheredoc_index.zero?heredoc_index>=index?index:heredoc_index+1end# @api privatedefwithin_column_limit?(element,max,line)element&&element.loc.column<=max&&element.loc.line==lineend# @api privatedefsafe_to_ignore?(node)returntrueunlessmaxreturntrueifalready_on_multiple_lines?(node)# If there's a containing breakable collection on the same# line, we let that one get broken first. In a separate pass,# this one might get broken as well, but to avoid conflicting# or redundant edits, we only mark one offense at a time.returntrueifcontained_by_breakable_collection_on_same_line?(node)returntrueifcontained_by_multiline_collection_that_could_be_broken_up?(node)falseend# @api privatedefbreakable_collection?(node,elements)# For simplicity we only want to insert breaks in normal# hashes wrapped in a set of curly braces like {foo: 1}.# That is, not a kwargs hash. For method calls, this ensures# the method call is made with parens.starts_with_bracket=!node.hash_type?||node.loc.begin# If the call has a second argument, we can insert a line# break before the second argument and the rest of the# argument will get auto-formatted onto separate lines# by other cops.has_second_element=elements.length>=2starts_with_bracket&&has_second_elementend# @api privatedefcontained_by_breakable_collection_on_same_line?(node)node.each_ancestor.finddo|ancestor|# Ignore ancestors on different lines.breakifancestor.first_line!=node.first_lineifancestor.hash_type?||ancestor.array_type?elements=ancestor.childrenelsifancestor.send_type?elements=process_args(ancestor.arguments)elsenextendreturntrueifbreakable_collection?(ancestor,elements)endfalseend# @api privatedefcontained_by_multiline_collection_that_could_be_broken_up?(node)node.each_ancestor.finddo|ancestor|if(ancestor.hash_type?||ancestor.array_type?)&&breakable_collection?(ancestor,ancestor.children)returnchildren_could_be_broken_up?(ancestor.children)endnextunlessancestor.send_type?args=process_args(ancestor.arguments)returnchildren_could_be_broken_up?(args)ifbreakable_collection?(ancestor,args)endfalseend# @api privatedefchildren_could_be_broken_up?(children)returnfalseifall_on_same_line?(children)last_seen_line=-1children.eachdo|child|returntrueiflast_seen_line>=child.first_linelast_seen_line=child.last_lineendfalseend# @api privatedefall_on_same_line?(nodes)returntrueifnodes.empty?nodes.first.first_line==nodes.last.last_lineend# @api privatedefprocess_args(args)# If there is a trailing hash arg without explicit braces, like this:## method(1, 'key1' => value1, 'key2' => value2)## ...then each key/value pair is treated as a method 'argument'# when determining where line breaks should appear.last_arg=args.lastargs=args[0...-1]+last_arg.childreniflast_arg&.hash_type?&&!last_arg&.braces?argsend# @api privatedefalready_on_multiple_lines?(node)returnnode.first_line!=node.arguments.last.last_lineifnode.def_type?!node.single_line?endendendend