Module: RuboCop::Cop::CheckLineBreakable
| Relationships & Source Files | |
| Extension / Inclusion / Inheritance Descendants | |
|
Included In:
| |
| Defined in: | lib/rubocop/cop/mixin/check_line_breakable.rb |
Overview
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:
"0000000000", bar: "0000000000",
baz: "0000000000"
In a MultilineHashKeyLineBreaks pass, lines are inserted before all keys:
"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.)
Instance Method Summary
- #extract_breakable_node(node, max)
- #all_on_same_line?(nodes) ⇒ Boolean private Internal use only
- #already_on_multiple_lines?(node) ⇒ Boolean private Internal use only
- #breakable_collection?(node, elements) ⇒ Boolean private Internal use only
- #chained_to_heredoc?(node) ⇒ Boolean private
-
#children_could_be_broken_up?(node, children) ⇒ Boolean
private
Internal use only
The result depends only on the given node, but the check may traverse all children of a large collection and is repeated for each contained element, so memoize it per node.
- #compute_children_could_be_broken_up?(children) ⇒ Boolean private Internal use only
- #contained_by_breakable_collection_on_same_line?(node) ⇒ Boolean private Internal use only
- #contained_by_multiline_collection_that_could_be_broken_up?(node) ⇒ Boolean private Internal use only
- #extract_breakable_node_from_elements(node, elements, max) private Internal use only
- #extract_first_element_over_column_limit(node, elements, max) private Internal use only
- #first_argument_is_heredoc?(node) ⇒ Boolean private Internal use only
- #heredoc_begin_position(node) private Internal use only
- #process_args(args) private Internal use only
- #safe_to_ignore?(node) ⇒ Boolean private Internal use only
-
#shift_elements_for_heredoc_arg(node, elements, index)
private
Internal use only
If a
sendorcsendnode contains a heredoc argument, splitting cannot happen after the heredoc or else it will cause a syntax error. - #within_column_limit?(element, max, line) ⇒ Boolean private Internal use only
Instance Method Details
#all_on_same_line?(nodes) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 223
def all_on_same_line?(nodes) return true if nodes.empty? nodes.first.first_line == nodes.last.last_line end
#already_on_multiple_lines?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 243
def already_on_multiple_lines?(node) return node.first_line != node.last_argument.last_line if node.any_def_type? node.multiline? end
#breakable_collection?(node, elements) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 142
def breakable_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. starts_with_bracket && elements.length >= 2 end
#chained_to_heredoc?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 249
def chained_to_heredoc?(node) while (node = node.receiver) return true if node.any_str_type? && node.heredoc? end false end
#children_could_be_broken_up?(node, children) ⇒ Boolean (private)
The result depends only on the given node, but the check may traverse all children of a large collection and is repeated for each contained element, so memoize it per node.
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 198
def children_could_be_broken_up?(node, children) if @check_line_breakable_processed_source != processed_source @check_line_breakable_processed_source = processed_source @children_could_be_broken_up_cache = {}.compare_by_identity end @children_could_be_broken_up_cache.fetch(node) do @children_could_be_broken_up_cache[node] = compute_children_could_be_broken_up?(children) end end
#compute_children_could_be_broken_up?(children) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 210
def compute_children_could_be_broken_up?(children) return false if all_on_same_line?(children) last_seen_line = -1 children.each do |child| return true if last_seen_line >= child.first_line last_seen_line = child.last_line end false end
#contained_by_breakable_collection_on_same_line?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 157
def contained_by_breakable_collection_on_same_line?(node) node.each_ancestor.find do |ancestor| # Ignore ancestors on different lines. break if ancestor.first_line != node.first_line if ancestor.type?(:hash, :array) elements = ancestor.children elsif ancestor.call_type? elements = process_args(ancestor.arguments) else next end return true if breakable_collection?(ancestor, elements) end false end
#contained_by_multiline_collection_that_could_be_broken_up?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 177
def contained_by_multiline_collection_that_could_be_broken_up?(node) node.each_ancestor.find do |ancestor| if ancestor.type?(:hash, :array) && breakable_collection?(ancestor, ancestor.children) return children_could_be_broken_up?(ancestor, ancestor.children) end next unless ancestor.call_type? args = process_args(ancestor.arguments) if breakable_collection?(ancestor, args) return children_could_be_broken_up?(ancestor, args) end end false end
#extract_breakable_node(node, max)
[ GitHub ]# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 45
def extract_breakable_node(node, max) if node.call_type? return if chained_to_heredoc?(node) args = process_args(node.arguments) return extract_breakable_node_from_elements(node, args, max) elsif node.any_def_type? return extract_breakable_node_from_elements(node, node.arguments, max) elsif node.type?(:array, :hash) return extract_breakable_node_from_elements(node, node.children, max) end nil end
#extract_breakable_node_from_elements(node, elements, max) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 62
def extract_breakable_node_from_elements(node, elements, max) return unless breakable_collection?(node, elements) # Check the cheap conditions first, `safe_to_ignore?` may traverse all elements of # a containing collection. line = processed_source.lines[node.first_line - 1] return if max && line.length <= max return if processed_source.line_with_comment?(node.loc.line) return if safe_to_ignore?(node) extract_first_element_over_column_limit(node, elements, max) end
#extract_first_element_over_column_limit(node, elements, max) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 76
def extract_first_element_over_column_limit(node, elements, max) line = node.first_line # If a `send` or `csend` node is not parenthesized, don't move the first element, because it # can result in changed behavior or a syntax error. if node.call_type? && !node.parenthesized? && !first_argument_is_heredoc?(node) elements = elements.drop(1) end i = 0 i += 1 while within_column_limit?(elements[i], max, line) i = shift_elements_for_heredoc_arg(node, elements, i) return if i.nil? return elements.first if i.zero? elements[i - 1] end
#first_argument_is_heredoc?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 96
def first_argument_is_heredoc?(node) node.first_argument.respond_to?(:heredoc?) && node.first_argument.heredoc? end
#heredoc_begin_position(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 115
def heredoc_begin_position(node) children = node.call_type? ? node.arguments : node.children children.flat_map { |child| [child, *child.each_descendant(:any_str)] } .find { |descendant| descendant.type?(:any_str) && descendant.heredoc? } &.source_range&.begin_pos end
#process_args(args) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 230
def process_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.last args = args[0...-1] + last_arg.children if last_arg&.hash_type? && !last_arg&.braces? args end
#safe_to_ignore?(node) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 128
def safe_to_ignore?(node) return true unless max return true if already_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. return true if contained_by_breakable_collection_on_same_line?(node) contained_by_multiline_collection_that_could_be_broken_up?(node) end
#shift_elements_for_heredoc_arg(node, elements, index) (private)
If a send or csend node contains a heredoc argument, splitting cannot happen
after the heredoc or else it will cause a syntax error.
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 103
def shift_elements_for_heredoc_arg(node, elements, index) return index unless node.type?(:call, :array, :hash) return index unless (heredoc_pos = heredoc_begin_position(node)) last_safe = elements.rindex { |element| element.source_range.begin_pos <= heredoc_pos } return nil if last_safe.nil? || (last_safe.zero? && elements.first.source_range.end_pos > heredoc_pos) [index, last_safe + 1].min end
#within_column_limit?(element, max, line) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/check_line_breakable.rb', line 123
def within_column_limit?(element, max, line) element && element.loc.column <= max && element.loc.line == line end