rubocop-tablecop 0.2.0 → 0.3.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 +4 -4
- data/config/default.yml +5 -0
- data/lib/rubocop/cop/tablecop/condense_in.rb +255 -0
- data/lib/rubocop/cop/tablecop_cops.rb +1 -0
- data/lib/rubocop/tablecop/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e236fc9472fa86fdc2275fc0720618f42f7dc196f6e254c6c66242c2a444aa5
|
|
4
|
+
data.tar.gz: c8d7b7f15aa93e209fe546c884c10a6d546870faf29ac3e369ecbc23cc913eb0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de2321a97cd8f4858f925960d893ae7b30e364bcd646ba472a0655a45f4d025ec8525630337646954feeb0e2473d1244f38337b22cda5149c43dc9e650b5765b
|
|
7
|
+
data.tar.gz: ea87e5e4c098c91f4978571dbf4a4c135f45ad94aea8c402300dc54e1bf831abc5acbe2c73dd345dc647aaafe0d1f62cb8843df20b04d8b117014ac4f300d074
|
data/config/default.yml
CHANGED
|
@@ -21,6 +21,11 @@ Tablecop/AlignMethods:
|
|
|
21
21
|
Enabled: true
|
|
22
22
|
VersionAdded: "0.1.0"
|
|
23
23
|
|
|
24
|
+
Tablecop/CondenseIn:
|
|
25
|
+
Description: "Condense multi-line `in` clauses (pattern matching) to single lines when possible."
|
|
26
|
+
Enabled: true
|
|
27
|
+
VersionAdded: "0.1.0"
|
|
28
|
+
|
|
24
29
|
Tablecop/CondenseWhen:
|
|
25
30
|
Description: "Condense multi-line `when` clauses to single lines when possible."
|
|
26
31
|
Enabled: true
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Tablecop
|
|
6
|
+
# Checks for multi-line `in` clauses (pattern matching) that could be
|
|
7
|
+
# condensed to a single line using the `then` keyword, with alignment.
|
|
8
|
+
#
|
|
9
|
+
# This cop encourages a table-like, vertically-aligned style for pattern
|
|
10
|
+
# matching case statements where each in clause fits on one line.
|
|
11
|
+
#
|
|
12
|
+
# When guard clauses are present, three-column alignment is used:
|
|
13
|
+
# patterns align, then `if` keywords align, then `then` keywords align.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# case response
|
|
18
|
+
# in [:ok, body]
|
|
19
|
+
# handle_success(body)
|
|
20
|
+
# in [:error, code]
|
|
21
|
+
# handle_error(code)
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good (aligned)
|
|
25
|
+
# case response
|
|
26
|
+
# in [:ok, body] then handle_success(body)
|
|
27
|
+
# in [:error, code] then handle_error(code)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # good (with guards - three-column alignment)
|
|
31
|
+
# case response
|
|
32
|
+
# in [:ok, body] then handle_success(body)
|
|
33
|
+
# in [:error, code] if retry? then handle_retry(code)
|
|
34
|
+
# in [:error, code] then handle_error(code)
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # also good (body too complex for single line)
|
|
38
|
+
# case response
|
|
39
|
+
# in [:ok, body]
|
|
40
|
+
# log_success
|
|
41
|
+
# handle_success(body)
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
class CondenseIn < Base
|
|
45
|
+
extend AutoCorrector
|
|
46
|
+
|
|
47
|
+
MSG = "Condense `in` to single line with aligned `then`"
|
|
48
|
+
|
|
49
|
+
def on_case_match(node)
|
|
50
|
+
in_nodes = node.in_pattern_branches
|
|
51
|
+
return if in_nodes.empty?
|
|
52
|
+
|
|
53
|
+
# Analyze which in-patterns can be condensed
|
|
54
|
+
condensable = in_nodes.map { |n| [n, condensable?(n)] }
|
|
55
|
+
|
|
56
|
+
# If none can be condensed, nothing to do
|
|
57
|
+
return unless condensable.any? { |_, can| can }
|
|
58
|
+
|
|
59
|
+
# Calculate alignment widths from all condensable in-patterns
|
|
60
|
+
max_pattern_width = calculate_max_pattern_width(condensable)
|
|
61
|
+
max_guard_width = calculate_max_guard_width(condensable)
|
|
62
|
+
|
|
63
|
+
# Check if alignment would exceed line length for any condensable in-pattern
|
|
64
|
+
use_alignment = can_align_all?(condensable, max_pattern_width, max_guard_width, node)
|
|
65
|
+
|
|
66
|
+
# Register offenses and corrections for each condensable in-pattern
|
|
67
|
+
condensable.each do |in_node, can_condense|
|
|
68
|
+
next unless can_condense
|
|
69
|
+
next if in_node.single_line? # Already condensed
|
|
70
|
+
|
|
71
|
+
register_offense(in_node, max_pattern_width, max_guard_width, use_alignment, node)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def condensable?(node)
|
|
78
|
+
# Must have a body
|
|
79
|
+
return false unless node.body
|
|
80
|
+
|
|
81
|
+
# Body must be a simple single expression (not begin block with multiple statements)
|
|
82
|
+
return false if node.body.begin_type? && node.body.children.size > 1
|
|
83
|
+
|
|
84
|
+
# No heredocs
|
|
85
|
+
return false if contains_heredoc?(node.body)
|
|
86
|
+
|
|
87
|
+
# No multi-line strings
|
|
88
|
+
return false if contains_multiline_string?(node.body)
|
|
89
|
+
|
|
90
|
+
# No control flow that can't be safely condensed
|
|
91
|
+
return false if contains_complex_control_flow?(node.body)
|
|
92
|
+
|
|
93
|
+
# No comments between in and body
|
|
94
|
+
return false if comment_between?(node)
|
|
95
|
+
|
|
96
|
+
# Pattern must be on one line
|
|
97
|
+
return false unless pattern_single_line?(node)
|
|
98
|
+
|
|
99
|
+
# Guard (if present) must be on one line
|
|
100
|
+
return false unless guard_single_line?(node)
|
|
101
|
+
|
|
102
|
+
# Check if condensed form would exceed line length (without alignment)
|
|
103
|
+
single_line = build_single_line(node, 0, 0)
|
|
104
|
+
base_indent = node.loc.keyword.column
|
|
105
|
+
return false if (base_indent + single_line.length) > max_line_length
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def calculate_max_pattern_width(condensable)
|
|
111
|
+
condensable
|
|
112
|
+
.select { |_, can| can }
|
|
113
|
+
.map { |n, _| pattern_width(n) }
|
|
114
|
+
.max || 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def calculate_max_guard_width(condensable)
|
|
118
|
+
condensable
|
|
119
|
+
.select { |_, can| can }
|
|
120
|
+
.map { |n, _| guard_width(n) }
|
|
121
|
+
.max || 0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pattern_width(in_node)
|
|
125
|
+
in_node.pattern.source.length
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def guard_width(in_node)
|
|
129
|
+
guard = in_node.children[1]
|
|
130
|
+
return 0 unless guard
|
|
131
|
+
|
|
132
|
+
guard.source.length
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def can_align_all?(condensable, max_pattern_width, max_guard_width, case_node)
|
|
136
|
+
base_indent = case_node.loc.keyword.column
|
|
137
|
+
|
|
138
|
+
condensable.all? do |in_node, can_condense|
|
|
139
|
+
next true unless can_condense
|
|
140
|
+
next true if in_node.single_line?
|
|
141
|
+
|
|
142
|
+
# Check if aligned version fits
|
|
143
|
+
single_line = build_single_line(in_node, max_pattern_width, max_guard_width)
|
|
144
|
+
(base_indent + single_line.length) <= max_line_length
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_single_line(in_node, pad_pattern_to, pad_guard_to)
|
|
149
|
+
pattern_source = in_node.pattern.source
|
|
150
|
+
body_source = in_node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
151
|
+
guard = in_node.children[1]
|
|
152
|
+
|
|
153
|
+
if pad_pattern_to > 0
|
|
154
|
+
pattern_padding = " " * (pad_pattern_to - pattern_source.length)
|
|
155
|
+
|
|
156
|
+
if guard
|
|
157
|
+
guard_source = guard.source
|
|
158
|
+
guard_padding = " " * (pad_guard_to - guard_source.length)
|
|
159
|
+
"in #{pattern_source}#{pattern_padding} #{guard_source}#{guard_padding} then #{body_source}"
|
|
160
|
+
elsif pad_guard_to > 0
|
|
161
|
+
# No guard on this branch, but other branches have guards
|
|
162
|
+
# Pad to align with where 'then' would be after guards
|
|
163
|
+
total_padding = " " * (pad_pattern_to - pattern_source.length + pad_guard_to + 1)
|
|
164
|
+
"in #{pattern_source}#{total_padding} then #{body_source}"
|
|
165
|
+
else
|
|
166
|
+
"in #{pattern_source}#{pattern_padding} then #{body_source}"
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
if guard
|
|
170
|
+
"in #{pattern_source} #{guard.source} then #{body_source}"
|
|
171
|
+
else
|
|
172
|
+
"in #{pattern_source} then #{body_source}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def register_offense(in_node, max_pattern_width, max_guard_width, use_alignment, _case_node)
|
|
178
|
+
add_offense(in_node) do |corrector|
|
|
179
|
+
pattern_width = use_alignment ? max_pattern_width : 0
|
|
180
|
+
guard_width = use_alignment ? max_guard_width : 0
|
|
181
|
+
single_line = build_single_line(in_node, pattern_width, guard_width)
|
|
182
|
+
corrector.replace(in_node, single_line)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def pattern_single_line?(node)
|
|
187
|
+
pattern = node.pattern
|
|
188
|
+
pattern.first_line == pattern.last_line
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def guard_single_line?(node)
|
|
192
|
+
guard = node.children[1]
|
|
193
|
+
return true unless guard
|
|
194
|
+
|
|
195
|
+
guard.first_line == guard.last_line
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def contains_heredoc?(node)
|
|
199
|
+
return false unless node
|
|
200
|
+
|
|
201
|
+
return true if node.respond_to?(:heredoc?) && node.heredoc?
|
|
202
|
+
|
|
203
|
+
node.each_descendant(:str, :dstr, :xstr).any?(&:heredoc?)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def contains_multiline_string?(node)
|
|
207
|
+
return false unless node
|
|
208
|
+
|
|
209
|
+
node.each_descendant(:str, :dstr).any? do |str_node|
|
|
210
|
+
next false if str_node.heredoc?
|
|
211
|
+
|
|
212
|
+
str_node.source.include?("\n")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def contains_complex_control_flow?(node)
|
|
217
|
+
return false unless node
|
|
218
|
+
|
|
219
|
+
# Multi-line if/unless/case can't be condensed safely
|
|
220
|
+
if node.if_type? || node.case_type? || node.case_match_type?
|
|
221
|
+
return true unless node.single_line?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Check for multi-statement blocks
|
|
225
|
+
node.each_descendant(:block, :numblock) do |block_node|
|
|
226
|
+
return true if block_node.body&.begin_type?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Check descendants for complex control flow
|
|
230
|
+
node.each_descendant(:if, :case, :case_match) do |control_node|
|
|
231
|
+
return true unless control_node.single_line?
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def comment_between?(in_node)
|
|
238
|
+
return false unless in_node.body
|
|
239
|
+
|
|
240
|
+
comments = processed_source.comments
|
|
241
|
+
in_line = in_node.loc.keyword.line
|
|
242
|
+
body_line = in_node.body.first_line
|
|
243
|
+
|
|
244
|
+
comments.any? do |comment|
|
|
245
|
+
comment.loc.line.between?(in_line, body_line - 1)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def max_line_length
|
|
250
|
+
config.for_cop("Layout/LineLength")["Max"] || 120
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-tablecop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joseph Wecker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rubocop
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/rubocop-tablecop.rb
|
|
54
54
|
- lib/rubocop/cop/tablecop/align_assignments.rb
|
|
55
55
|
- lib/rubocop/cop/tablecop/align_methods.rb
|
|
56
|
+
- lib/rubocop/cop/tablecop/condense_in.rb
|
|
56
57
|
- lib/rubocop/cop/tablecop/condense_when.rb
|
|
57
58
|
- lib/rubocop/cop/tablecop/safe_endless_method.rb
|
|
58
59
|
- lib/rubocop/cop/tablecop_cops.rb
|