/
redundant_line_continuation.rb
199 lines (170 loc) · 5.87 KB
/
redundant_line_continuation.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
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Check for redundant line continuation.
#
# This cop marks a line continuation as redundant if removing the backslash
# does not result in a syntax error.
# However, a backslash at the end of a comment or
# for string concatenation is not redundant and is not considered an offense.
#
# @example
# # bad
# foo. \
# bar
# foo \
# &.bar \
# .baz
#
# # good
# foo.
# bar
# foo
# &.bar
# .baz
#
# # bad
# [foo, \
# bar]
# {foo: \
# bar}
#
# # good
# [foo,
# bar]
# {foo:
# bar}
#
# # bad
# foo(bar, \
# baz)
#
# # good
# foo(bar,
# baz)
#
# # also good - backslash in string concatenation is not redundant
# foo('bar' \
# 'baz')
#
# # also good - backslash at the end of a comment is not redundant
# foo(bar, # \
# baz)
#
# # also good - backslash at the line following the newline begins with a + or -,
# # it is not redundant
# 1 \
# + 2 \
# - 3
#
# # also good - backslash with newline between the method name and its arguments,
# # it is not redundant.
# some_method \
# (argument)
#
class RedundantLineContinuation < Base
include MatchRange
extend AutoCorrector
MSG = 'Redundant line continuation.'
ALLOWED_STRING_TOKENS = %i[tSTRING tSTRING_CONTENT].freeze
ARGUMENT_TYPES = %i[
kFALSE kNIL kSELF kTRUE tCONSTANT tCVAR tFLOAT tGVAR tIDENTIFIER tINTEGER tIVAR
tLBRACK tLCURLY tLPAREN_ARG tSTRING tSTRING_BEG tSYMBOL tXSTRING_BEG
].freeze
def on_new_investigation
return unless processed_source.ast
each_match_range(processed_source.ast.source_range, /(\\\n)/) do |range|
next if require_line_continuation?(range)
next unless redundant_line_continuation?(range)
add_offense(range) do |corrector|
corrector.remove_leading(range, 1)
end
end
end
private
def require_line_continuation?(range)
!ends_with_backslash_without_comment?(range.source_line) ||
string_concatenation?(range.source_line) ||
start_with_arithmetic_operator?(processed_source[range.line]) ||
inside_string_literal_or_method_with_argument?(range) ||
leading_dot_method_chain_with_blank_line?(range)
end
def ends_with_backslash_without_comment?(source_line)
source_line.gsub(/#.+/, '').end_with?('\\')
end
def string_concatenation?(source_line)
/["']\s*\\\z/.match?(source_line)
end
def inside_string_literal_or_method_with_argument?(range)
processed_source.tokens.each_cons(2).any? do |token, next_token|
next if token.line == next_token.line
inside_string_literal?(range, token) || method_with_argument?(token, next_token)
end
end
def leading_dot_method_chain_with_blank_line?(range)
return false unless range.source_line.strip.start_with?('.', '&.')
processed_source[range.line].strip.empty?
end
def redundant_line_continuation?(range)
return true unless (node = find_node_for_line(range.line))
return false if argument_newline?(node)
source = node.parent ? node.parent.source : node.source
parse(source.gsub("\\\n", "\n")).valid_syntax?
end
def inside_string_literal?(range, token)
ALLOWED_STRING_TOKENS.include?(token.type) && token.pos.overlaps?(range)
end
# A method call without parentheses such as the following cannot remove `\`:
#
# do_something \
# argument
def method_with_argument?(current_token, next_token)
return false if current_token.type != :tIDENTIFIER && current_token.type != :kRETURN
ARGUMENT_TYPES.include?(next_token.type)
end
# rubocop:disable Metrics/AbcSize
def argument_newline?(node)
node = node.to_a.last if node.assignment?
return false if node.parenthesized_call?
node = node.children.first if node.root? && node.begin_type?
if argument_is_method?(node)
argument_newline?(node.first_argument)
else
return false unless method_call_with_arguments?(node)
node.loc.selector.line != node.first_argument.loc.line
end
end
# rubocop:enable Metrics/AbcSize
def find_node_for_line(line)
processed_source.ast.each_node do |node|
return node if same_line?(node, line)
end
end
def same_line?(node, line)
return false unless (source_range = node.source_range)
if node.is_a?(AST::StrNode)
if node.heredoc?
(node.loc.heredoc_body.line..node.loc.heredoc_body.last_line).cover?(line)
else
(source_range.line..source_range.last_line).cover?(line)
end
else
source_range.line == line
end
end
def argument_is_method?(node)
return false unless node.send_type?
return false unless (first_argument = node.first_argument)
method_call_with_arguments?(first_argument)
end
def method_call_with_arguments?(node)
node.call_type? && !node.arguments.empty?
end
def start_with_arithmetic_operator?(source_line)
%r{\A\s*[+\-*/%]}.match?(source_line)
end
end
end
end
end