/
heredoc_argument_closing_parenthesis.rb
311 lines (262 loc) · 9.71 KB
/
heredoc_argument_closing_parenthesis.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# frozen_string_literal: true
module RuboCop
module Cop
module Layout
# Checks for the placement of the closing parenthesis
# in a method call that passes a HEREDOC string as an argument.
# It should be placed at the end of the line containing the
# opening HEREDOC tag.
#
# @example
# # bad
#
# foo(<<-SQL
# bar
# SQL
# )
#
# foo(<<-SQL, 123, <<-NOSQL,
# bar
# SQL
# baz
# NOSQL
# )
#
# foo(
# bar(<<-SQL
# baz
# SQL
# ),
# 123,
# )
#
# # good
#
# foo(<<-SQL)
# bar
# SQL
#
# foo(<<-SQL, 123, <<-NOSQL)
# bar
# SQL
# baz
# NOSQL
#
# foo(
# bar(<<-SQL),
# baz
# SQL
# 123,
# )
#
class HeredocArgumentClosingParenthesis < Base
include RangeHelp
extend AutoCorrector
MSG = 'Put the closing parenthesis for a method call with a ' \
'HEREDOC parameter on the same line as the HEREDOC opening.'
def self.autocorrect_incompatible_with
[Style::TrailingCommaInArguments]
end
def on_send(node)
heredoc_arg = extract_heredoc_argument(node)
return unless heredoc_arg
outermost_send = outermost_send_on_same_line(heredoc_arg)
return unless outermost_send
return if end_keyword_before_closing_parenthesis?(node)
return if subsequent_closing_parentheses_in_same_line?(outermost_send)
return if exist_argument_between_heredoc_end_and_closing_parentheses?(node)
add_offense(outermost_send.loc.end) do |corrector|
autocorrect(corrector, outermost_send)
end
end
private
# Autocorrection note:
#
# Commas are a bit tricky to handle when the method call is
# embedded in another expression. Here's an example:
#
# [
# first_array_value,
# foo(<<-SQL, 123, 456,
# SELECT * FROM db
# SQL
# ),
# third_array_value,
# ]
#
# The "internal" trailing comma is after `456`.
# The "external" trailing comma is after `)`.
#
# To autocorrect, we remove the latter, and move the former up:
#
# [
# first_array_value,
# foo(<<-SQL, 123, 456),
# SELECT * FROM db
# SQL
# third_array_value,
# ]
def autocorrect(corrector, node)
fix_closing_parenthesis(node, corrector)
remove_internal_trailing_comma(node, corrector) if internal_trailing_comma?(node)
fix_external_trailing_comma(node, corrector) if external_trailing_comma?(node)
end
def outermost_send_on_same_line(heredoc)
previous = heredoc
current = previous.parent
until send_missing_closing_parens?(current, previous, heredoc)
previous = current
current = current.parent
return unless previous && current
end
current
end
def send_missing_closing_parens?(parent, child, heredoc)
parent&.call_type? &&
parent.arguments.include?(child) &&
parent.loc.begin &&
parent.loc.end.line != heredoc.last_line
end
def extract_heredoc_argument(node)
node.arguments.find { |arg_node| extract_heredoc(arg_node) }
end
def extract_heredoc(node)
return node if heredoc_node?(node)
return node.receiver if single_line_send_with_heredoc_receiver?(node)
return unless node.hash_type?
node.values.find do |v|
heredoc = extract_heredoc(v)
return heredoc if heredoc
end
end
def heredoc_node?(node)
node.respond_to?(:heredoc?) && node.heredoc?
end
def single_line_send_with_heredoc_receiver?(node)
return false unless node.send_type?
return false unless heredoc_node?(node.receiver)
node.receiver.location.heredoc_end.end_pos > node.source_range.end_pos
end
# Closing parenthesis helpers.
def end_keyword_before_closing_parenthesis?(parenthesized_send_node)
parenthesized_send_node.ancestors.any? do |ancestor|
ancestor.loc.respond_to?(:end) && ancestor.loc.end&.source == 'end'
end
end
def subsequent_closing_parentheses_in_same_line?(outermost_send)
last_arg_of_outer_send = outermost_send.last_argument
return false unless last_arg_of_outer_send&.loc.respond_to?(:end) &&
(end_of_last_arg_of_outer_send = last_arg_of_outer_send.loc.end)
end_of_outer_send = outermost_send.loc.end
same_line?(end_of_outer_send, end_of_last_arg_of_outer_send) &&
end_of_outer_send.column == end_of_last_arg_of_outer_send.column + 1
end
def fix_closing_parenthesis(node, corrector)
remove_incorrect_closing_paren(node, corrector)
add_correct_closing_paren(node, corrector)
end
def add_correct_closing_paren(node, corrector)
corrector.insert_after(node.last_argument, ')')
end
def remove_incorrect_closing_paren(node, corrector)
corrector.remove(
range_between(
incorrect_parenthesis_removal_begin(node),
incorrect_parenthesis_removal_end(node)
)
)
end
def incorrect_parenthesis_removal_begin(node)
end_pos = node.source_range.end_pos
if safe_to_remove_line_containing_closing_paren?(node)
last_line_length = node.source.scan(/\n(.*)$/).last[0].size
end_pos - last_line_length - 1 # Add one for the line break itself.
else
end_pos - 1 # Just the `)` at the end of the string
end
end
def safe_to_remove_line_containing_closing_paren?(node)
last_line = processed_source[node.loc.end.line - 1]
# Safe to remove if last line only contains `)`, `,`, and whitespace.
last_line.match?(/^ *\) {0,20},{0,1} *$/)
end
def incorrect_parenthesis_removal_end(node)
end_pos = node.source_range.end_pos
if processed_source.buffer.source[end_pos] == ','
end_pos + 1
else
end_pos
end
end
def exist_argument_between_heredoc_end_and_closing_parentheses?(node)
return true unless node.loc.end
return false unless (heredoc_end = find_most_bottom_of_heredoc_end(node.arguments))
heredoc_end < node.loc.end.begin_pos &&
range_between(heredoc_end, node.loc.end.begin_pos).source.strip != ''
end
def find_most_bottom_of_heredoc_end(arguments)
arguments.filter_map do |argument|
argument.loc.heredoc_end.end_pos if argument.loc.respond_to?(:heredoc_end)
end.max
end
# Internal trailing comma helpers.
def remove_internal_trailing_comma(node, corrector)
offset = internal_trailing_comma_offset_from_last_arg(node)
last_arg_end_pos = node.children.last.source_range.end_pos
corrector.remove(range_between(last_arg_end_pos, last_arg_end_pos + offset))
end
def internal_trailing_comma?(node)
!internal_trailing_comma_offset_from_last_arg(node).nil?
end
# Returns nil if no trailing internal comma.
def internal_trailing_comma_offset_from_last_arg(node)
source_after_last_arg = range_between(
node.children.last.source_range.end_pos,
node.loc.end.begin_pos
).source
first_comma_offset = source_after_last_arg.index(',')
first_new_line_offset = source_after_last_arg.index("\n")
return if first_comma_offset.nil?
return if first_new_line_offset.nil?
return if first_comma_offset > first_new_line_offset
first_comma_offset + 1
end
# External trailing comma helpers.
def fix_external_trailing_comma(node, corrector)
remove_incorrect_external_trailing_comma(node, corrector)
add_correct_external_trailing_comma(node, corrector)
end
def add_correct_external_trailing_comma(node, corrector)
return unless external_trailing_comma?(node)
corrector.insert_after(node.last_argument, ',')
end
def remove_incorrect_external_trailing_comma(node, corrector)
end_pos = node.source_range.end_pos
return unless external_trailing_comma?(node)
corrector.remove(
range_between(
end_pos,
end_pos + external_trailing_comma_offset_from_loc_end(node)
)
)
end
def external_trailing_comma?(node)
!external_trailing_comma_offset_from_loc_end(node).nil?
end
# Returns nil if no trailing external comma.
def external_trailing_comma_offset_from_loc_end(node)
end_pos = node.source_range.end_pos
offset = 0
limit = 20
offset += 1 while offset < limit && space?(end_pos + offset)
char = processed_source.buffer.source[end_pos + offset]
return unless char == ','
offset + 1 # Add one to include the comma.
end
def space?(pos)
processed_source.buffer.source[pos] == ' '
end
end
end
end
end