rubocop-tablecop 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +63 -0
- data/LICENSE +21 -0
- data/README.md +433 -0
- data/config/default.yml +99 -0
- data/lib/rubocop/cop/tablecop/align_assignments.rb +240 -0
- data/lib/rubocop/cop/tablecop/align_methods.rb +140 -0
- data/lib/rubocop/cop/tablecop/condense_when.rb +207 -0
- data/lib/rubocop/cop/tablecop/safe_endless_method.rb +255 -0
- data/lib/rubocop/cop/tablecop_cops.rb +6 -0
- data/lib/rubocop/tablecop/plugin.rb +37 -0
- data/lib/rubocop/tablecop/version.rb +17 -0
- data/lib/rubocop/tablecop.rb +8 -0
- data/lib/rubocop-tablecop.rb +8 -0
- metadata +89 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Tablecop
|
|
6
|
+
# Aligns consecutive assignment statements on the `=` operator.
|
|
7
|
+
#
|
|
8
|
+
# Handles simple assignments (`=`), compound assignments (`||=`, `&&=`, `+=`, etc.),
|
|
9
|
+
# and constant assignments. Skips lines containing heredocs entirely to avoid
|
|
10
|
+
# infinite loops with Layout/SpaceAroundOperators.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# x = 1
|
|
15
|
+
# foo = 2
|
|
16
|
+
# barbaz = 3
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# x = 1
|
|
20
|
+
# foo = 2
|
|
21
|
+
# barbaz = 3
|
|
22
|
+
#
|
|
23
|
+
# @example Compound operators
|
|
24
|
+
# # bad
|
|
25
|
+
# data ||= attrs
|
|
26
|
+
# options = default
|
|
27
|
+
#
|
|
28
|
+
# # good
|
|
29
|
+
# data ||= attrs
|
|
30
|
+
# options = default
|
|
31
|
+
#
|
|
32
|
+
class AlignAssignments < Base
|
|
33
|
+
extend AutoCorrector
|
|
34
|
+
|
|
35
|
+
MSG = "Align assignment with other assignments in group"
|
|
36
|
+
|
|
37
|
+
# Assignment types we handle
|
|
38
|
+
ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn casgn op_asgn or_asgn and_asgn].freeze
|
|
39
|
+
|
|
40
|
+
def on_new_investigation
|
|
41
|
+
return if processed_source.blank?
|
|
42
|
+
|
|
43
|
+
groups = find_assignment_groups
|
|
44
|
+
groups.each { |group| check_group(group) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def find_assignment_groups
|
|
50
|
+
assignments = find_assignments
|
|
51
|
+
return [] if assignments.empty?
|
|
52
|
+
|
|
53
|
+
groups = []
|
|
54
|
+
current_group = [assignments.first]
|
|
55
|
+
|
|
56
|
+
assignments.each_cons(2) do |prev, curr|
|
|
57
|
+
if consecutive_lines?(prev, curr) && same_indent?(prev, curr)
|
|
58
|
+
current_group << curr
|
|
59
|
+
else
|
|
60
|
+
groups << current_group if current_group.size > 1
|
|
61
|
+
current_group = [curr]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
groups << current_group if current_group.size > 1
|
|
66
|
+
groups
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_assignments
|
|
70
|
+
return [] unless processed_source.ast
|
|
71
|
+
|
|
72
|
+
assignments = []
|
|
73
|
+
|
|
74
|
+
processed_source.ast.each_node(*ASSIGNMENT_TYPES) do |node|
|
|
75
|
+
# Skip if this assignment contains a heredoc
|
|
76
|
+
next if contains_heredoc?(node)
|
|
77
|
+
|
|
78
|
+
# Skip multi-assignment (a, b = ...)
|
|
79
|
+
# These have parent :mlhs (multiple left-hand side)
|
|
80
|
+
next if node.parent&.type == :masgn || node.parent&.type == :mlhs
|
|
81
|
+
|
|
82
|
+
# Skip inner lvasgn/ivasgn that are part of op_asgn/or_asgn/and_asgn
|
|
83
|
+
# (these compound assignments have the simple asgn as first child)
|
|
84
|
+
next if node.parent&.type&.to_s&.end_with?("_asgn") &&
|
|
85
|
+
%i[lvasgn ivasgn cvasgn gvasgn].include?(node.type)
|
|
86
|
+
|
|
87
|
+
# Skip array/hash element assignment (hash[:key] = val)
|
|
88
|
+
# These are actually send nodes with :[]= method
|
|
89
|
+
next if node.type == :send && node.method_name == :[]=
|
|
90
|
+
|
|
91
|
+
# Only include if it's on a single line
|
|
92
|
+
next unless node.single_line?
|
|
93
|
+
|
|
94
|
+
# Skip assignments inside blocks - they shouldn't align with
|
|
95
|
+
# assignments outside the block
|
|
96
|
+
next if inside_block?(node)
|
|
97
|
+
|
|
98
|
+
assignments << node
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
assignments.sort_by(&:first_line)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def consecutive_lines?(prev_node, curr_node)
|
|
105
|
+
curr_node.first_line == prev_node.last_line + 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def same_indent?(node1, node2)
|
|
109
|
+
indent(node1) == indent(node2)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def indent(node)
|
|
113
|
+
processed_source.lines[node.first_line - 1] =~ /\S/
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def check_group(group)
|
|
117
|
+
eq_cols = group.map { |node| equals_column(node) }
|
|
118
|
+
max_eq_col = eq_cols.max
|
|
119
|
+
|
|
120
|
+
return unless can_align_all?(group, eq_cols, max_eq_col)
|
|
121
|
+
|
|
122
|
+
group.each_with_index do |node, idx|
|
|
123
|
+
padding_needed = max_eq_col - eq_cols[idx]
|
|
124
|
+
next if padding_needed <= 0
|
|
125
|
+
|
|
126
|
+
register_offense(node, padding_needed)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def equals_column(node)
|
|
131
|
+
case node.type
|
|
132
|
+
when :lvasgn, :ivasgn, :cvasgn, :gvasgn
|
|
133
|
+
# Simple assignment: variable = value
|
|
134
|
+
# The operator loc is not directly available, find it from source
|
|
135
|
+
find_operator_column(node)
|
|
136
|
+
when :casgn
|
|
137
|
+
# Constant assignment: CONST = value
|
|
138
|
+
find_operator_column(node)
|
|
139
|
+
when :op_asgn, :or_asgn, :and_asgn
|
|
140
|
+
# Compound assignment: var += val, var ||= val, var &&= val
|
|
141
|
+
node.loc.operator.column
|
|
142
|
+
else
|
|
143
|
+
find_operator_column(node)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def find_operator_column(node)
|
|
148
|
+
# For simple assignments, find the = in the source
|
|
149
|
+
line = processed_source.lines[node.first_line - 1]
|
|
150
|
+
|
|
151
|
+
# Find the first = that's part of an assignment (not ==, !=, etc.)
|
|
152
|
+
# Start after the LHS
|
|
153
|
+
lhs_end = lhs_end_column(node)
|
|
154
|
+
|
|
155
|
+
# Search for = after LHS
|
|
156
|
+
rest_of_line = line[lhs_end..]
|
|
157
|
+
eq_match = rest_of_line&.match(/\s*(=)(?!=)/)
|
|
158
|
+
|
|
159
|
+
if eq_match
|
|
160
|
+
lhs_end + eq_match.begin(1)
|
|
161
|
+
else
|
|
162
|
+
# Fallback: use the LHS end position
|
|
163
|
+
lhs_end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def lhs_end_column(node)
|
|
168
|
+
case node.type
|
|
169
|
+
when :lvasgn
|
|
170
|
+
node.loc.name.end_pos - processed_source.buffer.line_range(node.first_line).begin_pos
|
|
171
|
+
when :ivasgn, :cvasgn, :gvasgn
|
|
172
|
+
node.loc.name.end_pos - processed_source.buffer.line_range(node.first_line).begin_pos
|
|
173
|
+
when :casgn
|
|
174
|
+
node.loc.name.end_pos - processed_source.buffer.line_range(node.first_line).begin_pos
|
|
175
|
+
else
|
|
176
|
+
0
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def padding_insert_position(node)
|
|
181
|
+
case node.type
|
|
182
|
+
when :op_asgn, :or_asgn, :and_asgn
|
|
183
|
+
node.loc.operator.begin_pos
|
|
184
|
+
else
|
|
185
|
+
# For simple assignments, insert before the =
|
|
186
|
+
line = processed_source.lines[node.first_line - 1]
|
|
187
|
+
line_start = processed_source.buffer.line_range(node.first_line).begin_pos
|
|
188
|
+
lhs_end = lhs_end_column(node)
|
|
189
|
+
|
|
190
|
+
rest_of_line = line[lhs_end..]
|
|
191
|
+
eq_match = rest_of_line&.match(/\s*(=)(?!=)/)
|
|
192
|
+
|
|
193
|
+
if eq_match
|
|
194
|
+
line_start + lhs_end + eq_match.begin(1)
|
|
195
|
+
else
|
|
196
|
+
line_start + lhs_end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def can_align_all?(group, eq_cols, max_eq_col)
|
|
202
|
+
group.each_with_index.all? do |node, idx|
|
|
203
|
+
padding = max_eq_col - eq_cols[idx]
|
|
204
|
+
line_length(node) + padding <= max_line_length
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def line_length(node)
|
|
209
|
+
processed_source.lines[node.first_line - 1].length
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def contains_heredoc?(node)
|
|
213
|
+
return false unless node
|
|
214
|
+
|
|
215
|
+
node.each_descendant(:str, :dstr, :xstr).any? do |str_node|
|
|
216
|
+
str_node.heredoc?
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def inside_block?(node)
|
|
221
|
+
node.each_ancestor(:block, :numblock).any?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def register_offense(node, padding_needed)
|
|
225
|
+
add_offense(node) do |corrector|
|
|
226
|
+
pos = padding_insert_position(node)
|
|
227
|
+
corrector.insert_before(
|
|
228
|
+
Parser::Source::Range.new(processed_source.buffer, pos, pos),
|
|
229
|
+
" " * padding_needed
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def max_line_length
|
|
235
|
+
config.for_cop("Layout/LineLength")["Max"] || 120
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Tablecop
|
|
6
|
+
# Aligns contiguous single-line method definitions so their bodies start
|
|
7
|
+
# at the same column. Aligns on `=` for endless methods; traditional
|
|
8
|
+
# one-liners align as if they had an invisible `=`.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# def foo = 1
|
|
13
|
+
# def barbaz = 2
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# def foo = 1
|
|
17
|
+
# def barbaz = 2
|
|
18
|
+
#
|
|
19
|
+
# @example Mixed endless and traditional
|
|
20
|
+
# # bad
|
|
21
|
+
# def foo = 1
|
|
22
|
+
# def bar() 2 end
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# def foo = 1
|
|
26
|
+
# def bar() 2 end
|
|
27
|
+
#
|
|
28
|
+
class AlignMethods < Base
|
|
29
|
+
extend AutoCorrector
|
|
30
|
+
|
|
31
|
+
MSG = "Align method body with other methods in group"
|
|
32
|
+
|
|
33
|
+
def on_new_investigation
|
|
34
|
+
return if processed_source.blank?
|
|
35
|
+
|
|
36
|
+
groups = find_method_groups
|
|
37
|
+
groups.each { |group| check_group(group) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def find_method_groups
|
|
43
|
+
methods = single_line_methods
|
|
44
|
+
return [] if methods.empty?
|
|
45
|
+
|
|
46
|
+
groups = []
|
|
47
|
+
current_group = [methods.first]
|
|
48
|
+
|
|
49
|
+
methods.each_cons(2) do |prev, curr|
|
|
50
|
+
if consecutive_lines?(prev, curr) && same_indent?(prev, curr)
|
|
51
|
+
current_group << curr
|
|
52
|
+
else
|
|
53
|
+
groups << current_group if current_group.size > 1
|
|
54
|
+
current_group = [curr]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
groups << current_group if current_group.size > 1
|
|
59
|
+
groups
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def single_line_methods
|
|
63
|
+
return [] unless processed_source.ast
|
|
64
|
+
|
|
65
|
+
processed_source.ast.each_descendant(:def).select(&:single_line?).sort_by(&:first_line)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def consecutive_lines?(prev_node, curr_node)
|
|
69
|
+
curr_node.first_line == prev_node.last_line + 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def same_indent?(node1, node2)
|
|
73
|
+
node1.loc.keyword.column == node2.loc.keyword.column
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_group(group)
|
|
77
|
+
# Calculate the column of `=` (or where `=` would be) for each method
|
|
78
|
+
eq_cols = group.map { |node| equals_column(node) }
|
|
79
|
+
max_eq_col = eq_cols.max
|
|
80
|
+
|
|
81
|
+
# Check if alignment would exceed line length for any method
|
|
82
|
+
return unless can_align_all?(group, eq_cols, max_eq_col)
|
|
83
|
+
|
|
84
|
+
group.each_with_index do |node, idx|
|
|
85
|
+
padding_needed = max_eq_col - eq_cols[idx]
|
|
86
|
+
next if padding_needed <= 0
|
|
87
|
+
|
|
88
|
+
register_offense(node, padding_needed)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the column where `=` is (endless) or would be (traditional)
|
|
93
|
+
def equals_column(node)
|
|
94
|
+
if node.endless?
|
|
95
|
+
node.loc.assignment.column
|
|
96
|
+
else
|
|
97
|
+
# For traditional: body column - 2 (pretend there's ` = ` before body)
|
|
98
|
+
# Actually, we want to align bodies, so use body column directly
|
|
99
|
+
# but we insert padding before body, effectively making ` = ` align
|
|
100
|
+
node.body.loc.expression.column - 2
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Position to insert padding
|
|
105
|
+
def padding_insert_position(node)
|
|
106
|
+
if node.endless?
|
|
107
|
+
node.loc.assignment.begin_pos
|
|
108
|
+
else
|
|
109
|
+
node.body.loc.expression.begin_pos
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def can_align_all?(group, eq_cols, max_eq_col)
|
|
114
|
+
group.each_with_index.all? do |node, idx|
|
|
115
|
+
padding = max_eq_col - eq_cols[idx]
|
|
116
|
+
line_length(node) + padding <= max_line_length
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def line_length(node)
|
|
121
|
+
processed_source.lines[node.first_line - 1].length
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def register_offense(node, padding_needed)
|
|
125
|
+
add_offense(node) do |corrector|
|
|
126
|
+
pos = padding_insert_position(node)
|
|
127
|
+
corrector.insert_before(
|
|
128
|
+
Parser::Source::Range.new(processed_source.buffer, pos, pos),
|
|
129
|
+
" " * padding_needed
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def max_line_length
|
|
135
|
+
config.for_cop("Layout/LineLength")["Max"] || 120
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Tablecop
|
|
6
|
+
# Checks for multi-line `when` clauses that could be condensed to a single
|
|
7
|
+
# line using the `then` keyword, and aligns `then` keywords across siblings.
|
|
8
|
+
#
|
|
9
|
+
# This cop encourages a table-like, vertically-aligned style for case
|
|
10
|
+
# statements where each when clause fits on one line.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# case foo
|
|
15
|
+
# when 1
|
|
16
|
+
# "one"
|
|
17
|
+
# when 200
|
|
18
|
+
# "two hundred"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good (aligned)
|
|
22
|
+
# case foo
|
|
23
|
+
# when 1 then "one"
|
|
24
|
+
# when 200 then "two hundred"
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # also good (body too complex for single line)
|
|
28
|
+
# case foo
|
|
29
|
+
# when 1
|
|
30
|
+
# do_something
|
|
31
|
+
# do_something_else
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
class CondenseWhen < Base
|
|
35
|
+
extend AutoCorrector
|
|
36
|
+
|
|
37
|
+
MSG = "Condense `when` to single line with aligned `then`"
|
|
38
|
+
|
|
39
|
+
def on_case(node)
|
|
40
|
+
when_nodes = node.when_branches
|
|
41
|
+
return if when_nodes.empty?
|
|
42
|
+
|
|
43
|
+
# Analyze which whens can be condensed
|
|
44
|
+
condensable = when_nodes.map { |w| [w, condensable?(w)] }
|
|
45
|
+
|
|
46
|
+
# If none can be condensed, nothing to do
|
|
47
|
+
return unless condensable.any? { |_, can| can }
|
|
48
|
+
|
|
49
|
+
# Calculate alignment width from all condensable whens
|
|
50
|
+
max_condition_width = calculate_max_condition_width(condensable)
|
|
51
|
+
|
|
52
|
+
# Check if alignment would exceed line length for any condensable when
|
|
53
|
+
use_alignment = can_align_all?(condensable, max_condition_width, node)
|
|
54
|
+
|
|
55
|
+
# Register offenses and corrections for each condensable when
|
|
56
|
+
condensable.each do |when_node, can_condense|
|
|
57
|
+
next unless can_condense
|
|
58
|
+
next if when_node.single_line? # Already condensed
|
|
59
|
+
|
|
60
|
+
register_offense(when_node, max_condition_width, use_alignment, node)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def condensable?(node)
|
|
67
|
+
# Must have a body
|
|
68
|
+
return false unless node.body
|
|
69
|
+
|
|
70
|
+
# Body must be a simple single expression (not begin block with multiple statements)
|
|
71
|
+
return false if node.body.begin_type? && node.body.children.size > 1
|
|
72
|
+
|
|
73
|
+
# No heredocs
|
|
74
|
+
return false if contains_heredoc?(node.body)
|
|
75
|
+
|
|
76
|
+
# No multi-line strings
|
|
77
|
+
return false if contains_multiline_string?(node.body)
|
|
78
|
+
|
|
79
|
+
# No control flow that can't be safely condensed
|
|
80
|
+
# (if/unless/case with else, multi-statement blocks, etc.)
|
|
81
|
+
return false if contains_complex_control_flow?(node.body)
|
|
82
|
+
|
|
83
|
+
# No comments between when and body
|
|
84
|
+
return false if comment_between?(node)
|
|
85
|
+
|
|
86
|
+
# Conditions must be on one line
|
|
87
|
+
return false unless conditions_single_line?(node)
|
|
88
|
+
|
|
89
|
+
# Check if condensed form would exceed line length (without alignment)
|
|
90
|
+
single_line = build_single_line(node, 0)
|
|
91
|
+
base_indent = node.loc.keyword.column
|
|
92
|
+
return false if (base_indent + single_line.length) > max_line_length
|
|
93
|
+
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def calculate_max_condition_width(condensable)
|
|
98
|
+
condensable
|
|
99
|
+
.select { |_, can| can }
|
|
100
|
+
.map { |w, _| condition_width(w) }
|
|
101
|
+
.max || 0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def condition_width(when_node)
|
|
105
|
+
when_node.conditions.map(&:source).join(", ").length
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def can_align_all?(condensable, max_width, case_node)
|
|
109
|
+
base_indent = case_node.loc.keyword.column
|
|
110
|
+
|
|
111
|
+
condensable.all? do |when_node, can_condense|
|
|
112
|
+
next true unless can_condense
|
|
113
|
+
next true if when_node.single_line?
|
|
114
|
+
|
|
115
|
+
# Check if aligned version fits
|
|
116
|
+
single_line = build_single_line(when_node, max_width)
|
|
117
|
+
(base_indent + single_line.length) <= max_line_length
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_single_line(when_node, pad_to_width)
|
|
122
|
+
conditions_source = when_node.conditions.map(&:source).join(", ")
|
|
123
|
+
body_source = when_node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
124
|
+
|
|
125
|
+
if pad_to_width > 0
|
|
126
|
+
padding = " " * (pad_to_width - conditions_source.length)
|
|
127
|
+
"when #{conditions_source}#{padding} then #{body_source}"
|
|
128
|
+
else
|
|
129
|
+
"when #{conditions_source} then #{body_source}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def register_offense(when_node, max_width, use_alignment, _case_node)
|
|
134
|
+
add_offense(when_node) do |corrector|
|
|
135
|
+
width = use_alignment ? max_width : 0
|
|
136
|
+
single_line = build_single_line(when_node, width)
|
|
137
|
+
corrector.replace(when_node, single_line)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def conditions_single_line?(node)
|
|
142
|
+
return true if node.conditions.size == 1
|
|
143
|
+
|
|
144
|
+
first_cond = node.conditions.first
|
|
145
|
+
last_cond = node.conditions.last
|
|
146
|
+
first_cond.first_line == last_cond.last_line
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def contains_heredoc?(node)
|
|
150
|
+
return false unless node
|
|
151
|
+
|
|
152
|
+
# Check the node itself if it's a string type
|
|
153
|
+
return true if node.respond_to?(:heredoc?) && node.heredoc?
|
|
154
|
+
|
|
155
|
+
node.each_descendant(:str, :dstr, :xstr).any?(&:heredoc?)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def contains_multiline_string?(node)
|
|
159
|
+
return false unless node
|
|
160
|
+
|
|
161
|
+
node.each_descendant(:str, :dstr).any? do |str_node|
|
|
162
|
+
next false if str_node.heredoc?
|
|
163
|
+
|
|
164
|
+
str_node.source.include?("\n")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def contains_complex_control_flow?(node)
|
|
169
|
+
return false unless node
|
|
170
|
+
|
|
171
|
+
# Multi-line if/unless/case can't be condensed safely
|
|
172
|
+
if node.if_type? || node.case_type?
|
|
173
|
+
return true unless node.single_line?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check for multi-statement blocks
|
|
177
|
+
node.each_descendant(:block, :numblock) do |block_node|
|
|
178
|
+
return true if block_node.body&.begin_type?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Check descendants for complex control flow
|
|
182
|
+
node.each_descendant(:if, :case) do |control_node|
|
|
183
|
+
return true unless control_node.single_line?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def comment_between?(when_node)
|
|
190
|
+
return false unless when_node.body
|
|
191
|
+
|
|
192
|
+
comments = processed_source.comments
|
|
193
|
+
when_line = when_node.loc.keyword.line
|
|
194
|
+
body_line = when_node.body.first_line
|
|
195
|
+
|
|
196
|
+
comments.any? do |comment|
|
|
197
|
+
comment.loc.line.between?(when_line, body_line - 1)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def max_line_length
|
|
202
|
+
config.for_cop("Layout/LineLength")["Max"] || 120
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|