forked from rubocop/rubocop
/
check_line_breakable.rb
182 lines (152 loc) · 5.86 KB
/
check_line_breakable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# frozen_string_literal: true
module RuboCop
module Cop
# 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.)
module CheckLineBreakable
def extract_breakable_node(node, max)
if node.send_type?
args = process_args(node.arguments)
return extract_breakable_node_from_elements(node, args, max)
elsif node.array_type? || node.hash_type?
return extract_breakable_node_from_elements(node, node.children, max)
end
nil
end
private
def extract_breakable_node_from_elements(node, elements, max)
return unless breakable_collection?(node, elements)
return if safe_to_ignore?(node)
line = processed_source.lines[node.first_line - 1]
return if processed_source.line_with_comment?(node.loc.line)
return if line.length <= max
extract_first_element_over_column_limit(node, elements, max)
end
def extract_first_element_over_column_limit(node, elements, max)
line = node.first_line
i = 0
i += 1 while within_column_limit?(elements[i], max, line)
return elements.first if i.zero?
elements[i - 1]
end
def within_column_limit?(element, max, line)
element && element.loc.column < max && element.loc.line == line
end
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)
return true if contained_by_multiline_collection_that_could_be_broken_up?(node)
false
end
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.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 >= 2
starts_with_bracket && has_second_element
end
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.hash_type? || ancestor.array_type?
elements = ancestor.children
elsif ancestor.send_type?
elements = process_args(ancestor.arguments)
else
next
end
return true if breakable_collection?(ancestor, elements)
end
false
end
def contained_by_multiline_collection_that_could_be_broken_up?(node)
node.each_ancestor.find do |ancestor|
if (ancestor.hash_type? || ancestor.array_type?) &&
breakable_collection?(ancestor, ancestor.children)
return children_could_be_broken_up?(ancestor.children)
end
next unless ancestor.send_type?
args = process_args(ancestor.arguments)
return children_could_be_broken_up?(args) if breakable_collection?(ancestor, args)
end
false
end
def 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
def all_on_same_line?(nodes)
return true if nodes.empty?
nodes.first.first_line == nodes.last.last_line
end
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
def already_on_multiple_lines?(node)
node.first_line != node.last_line
end
end
end
end