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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51eef2c0d5feffe748c3522a755b445f04c3a4b3375ae0aea80fed3abac7e3c6
4
- data.tar.gz: d970c3c03b7cf9e4dc01c3184e276e79aaf7e88651d651f5f7d0b444ba7bad21
3
+ metadata.gz: 8e236fc9472fa86fdc2275fc0720618f42f7dc196f6e254c6c66242c2a444aa5
4
+ data.tar.gz: c8d7b7f15aa93e209fe546c884c10a6d546870faf29ac3e369ecbc23cc913eb0
5
5
  SHA512:
6
- metadata.gz: 19bed6636d3be68d97d506d660dab28f2470eb2e6f3dc94312809a32ee7909a66d914e3a1995e65278f38e4c8b21c4365cde21839528be48748dad27661bde65
7
- data.tar.gz: e0c333e488389fee8c0cace9506b429e3eff5fed20b503ec2972ecbe5030c2b4791ba91cd5f8518e90338d48c93064a4c28b83e0f1507bf57bd7877d8be60b4d
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
@@ -2,5 +2,6 @@
2
2
 
3
3
  require_relative "tablecop/align_assignments"
4
4
  require_relative "tablecop/align_methods"
5
+ require_relative "tablecop/condense_in"
5
6
  require_relative "tablecop/condense_when"
6
7
  require_relative "tablecop/safe_endless_method"
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Tablecop
5
5
  # Version information for rubocop-tablecop.
6
6
  module Version
7
- STRING = "0.2.0"
7
+ STRING = "0.3.0"
8
8
 
9
9
  def self.document_version
10
10
  STRING.match('\d+\.\d+').to_s
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.2.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-14 00:00:00.000000000 Z
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