/
line_length.rb
280 lines (234 loc) · 8.42 KB
/
line_length.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# frozen_string_literal: true
require 'uri'
module RuboCop
module Cop
module Layout
# This cop checks the length of lines in the source code.
# The maximum length is configurable.
# The tab size is configured in the `IndentationWidth`
# of the `Layout/IndentationStyle` cop.
# It also ignores a shebang line by default.
#
# This cop has some autocorrection capabilities.
# It can programmatically shorten certain long lines by
# inserting line breaks into expressions that can be safely
# split across lines. These include arrays, hashes, and
# method calls with argument lists.
#
# If autocorrection is enabled, the following Layout cops
# are recommended to further format the broken lines.
# (Many of these are enabled by default.)
#
# - ArgumentAlignment
# - BlockAlignment
# - BlockDelimiters
# - BlockEndNewline
# - ClosingParenthesisIndentation
# - FirstArgumentIndentation
# - FirstArrayElementIndentation
# - FirstHashElementIndentation
# - FirstParameterIndentation
# - HashAlignment
# - IndentationWidth
# - MultilineArrayLineBreaks
# - MultilineBlockLayout
# - MultilineHashBraceLayout
# - MultilineHashKeyLineBreaks
# - MultilineMethodArgumentLineBreaks
# - ParameterAlignment
#
# Together, these cops will pretty print hashes, arrays,
# method calls, etc. For example, let's say the max columns
# is 25:
#
# @example
#
# # bad
# {foo: "0000000000", bar: "0000000000", baz: "0000000000"}
#
# # good
# {foo: "0000000000",
# bar: "0000000000", baz: "0000000000"}
#
# # good (with recommended cops enabled)
# {
# foo: "0000000000",
# bar: "0000000000",
# baz: "0000000000",
# }
class LineLength < Cop
include CheckLineBreakable
include ConfigurableMax
include IgnoredPattern
include RangeHelp
include LineLengthHelp
MSG = 'Line is too long. [%<length>d/%<max>d]'
def on_block(node)
check_for_breakable_block(node)
end
def on_potential_breakable_node(node)
check_for_breakable_node(node)
end
alias on_array on_potential_breakable_node
alias on_hash on_potential_breakable_node
alias on_send on_potential_breakable_node
def investigate(processed_source)
check_for_breakable_semicolons(processed_source)
end
def investigate_post_walk(processed_source)
processed_source.lines.each_with_index do |line, line_index|
check_line(line, line_index)
end
end
def autocorrect(range)
return if range.nil?
lambda do |corrector|
corrector.insert_before(range, "\n")
end
end
private
def check_for_breakable_node(node)
breakable_node = extract_breakable_node(node, max)
return if breakable_node.nil?
line_index = breakable_node.first_line - 1
range = breakable_node.source_range
existing = breakable_range_by_line_index[line_index]
return if existing
breakable_range_by_line_index[line_index] = range
end
def check_for_breakable_semicolons(processed_source)
tokens = processed_source.tokens.select { |t| t.type == :tSEMI }
tokens.reverse_each do |token|
range = breakable_range_after_semicolon(token)
breakable_range_by_line_index[range.line - 1] = range if range
end
end
def check_for_breakable_block(block_node)
return unless block_node.single_line?
line_index = block_node.loc.line - 1
range = breakable_block_range(block_node)
pos = range.begin_pos + 1
breakable_range_by_line_index[line_index] =
range_between(pos, pos + 1)
end
def breakable_block_range(block_node)
if block_node.arguments? && !block_node.lambda?
block_node.arguments.loc.end
else
block_node.loc.begin
end
end
def breakable_range_after_semicolon(semicolon_token)
range = semicolon_token.pos
end_pos = range.end_pos
next_range = range_between(end_pos, end_pos + 1)
return nil unless next_range.line == range.line
next_char = next_range.source
return nil if /[\r\n]/.match?(next_char)
return nil if next_char == ';'
next_range
end
def breakable_range_by_line_index
@breakable_range_by_line_index ||= {}
end
def heredocs
@heredocs ||= extract_heredocs(processed_source.ast)
end
def highlight_start(line)
# TODO: The max with 0 is a quick fix to avoid crashes when a line
# begins with many tabs, but getting a correct highlighting range
# when tabs are used for indentation doesn't work currently.
[max - indentation_difference(line), 0].max
end
def check_line(line, line_index)
return if line_length(line) <= max
return if ignored_line?(line, line_index)
if ignore_cop_directives? && directive_on_source_line?(line_index)
return check_directive_line(line, line_index)
end
return check_uri_line(line, line_index) if allow_uri?
register_offense(
excess_range(nil, line, line_index),
line,
line_index
)
end
def ignored_line?(line, line_index)
matches_ignored_pattern?(line) ||
shebang?(line, line_index) ||
heredocs && line_in_permitted_heredoc?(line_index.succ)
end
def shebang?(line, line_index)
line_index.zero? && line.start_with?('#!')
end
def register_offense(loc, line, line_index)
message = format(MSG, length: line_length(line), max: max)
breakable_range = breakable_range_by_line_index[line_index]
add_offense(breakable_range, location: loc, message: message) do
self.max = line_length(line)
end
end
def excess_range(uri_range, line, line_index)
excessive_position = if uri_range && uri_range.begin < max
uri_range.end
else
highlight_start(line)
end
source_range(processed_source.buffer, line_index + 1,
excessive_position...(line_length(line)))
end
def max
cop_config['Max']
end
def allow_heredoc?
allowed_heredoc
end
def allowed_heredoc
cop_config['AllowHeredoc']
end
def extract_heredocs(ast)
return [] unless ast
ast.each_node(:str, :dstr, :xstr).select(&:heredoc?).map do |node|
body = node.location.heredoc_body
delimiter = node.location.heredoc_end.source.strip
[body.first_line...body.last_line, delimiter]
end
end
def line_in_permitted_heredoc?(line_number)
return false unless allowed_heredoc
heredocs.any? do |range, delimiter|
range.cover?(line_number) &&
(allowed_heredoc == true || allowed_heredoc.include?(delimiter))
end
end
def line_in_heredoc?(line_number)
heredocs.any? do |range, _delimiter|
range.cover?(line_number)
end
end
def check_directive_line(line, line_index)
return if line_length_without_directive(line) <= max
range = max..(line_length_without_directive(line) - 1)
register_offense(
source_range(
processed_source.buffer,
line_index + 1,
range
),
line,
line_index
)
end
def check_uri_line(line, line_index)
uri_range = find_excessive_uri_range(line)
return if uri_range && allowed_uri_position?(line, uri_range)
register_offense(
excess_range(uri_range, line, line_index),
line,
line_index
)
end
end
end
end
end