rubocop-tablecop 0.2.0 → 0.3.1
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/CHANGELOG.md +16 -0
- data/config/default.yml +5 -0
- data/lib/rubocop/cop/tablecop/condense_in.rb +255 -0
- data/lib/rubocop/cop/tablecop/safe_endless_method.rb +23 -0
- data/lib/rubocop/cop/tablecop_cops.rb +1 -0
- data/lib/rubocop/tablecop/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b4eae624919db229c2973bf708c98fbb3493ae26c1442df19daded0b9f8b598
|
|
4
|
+
data.tar.gz: 1c0f7a64dc50fec124730b4d45c2a9b49044e05bad6984a91f45db43c5188e15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cd4fae66c17a7da85a859414f627475dc012902ceb4848a1ef84c9abd1c0236faad97ffdae718c3c9a635d1946d3b6c2a5c1e81734fd84f8916d32ad7d5e13e
|
|
7
|
+
data.tar.gz: 8cee4c0a8e542bebc8c7ea4e8a50fc4bb6c4cac8936f423cb45b7d08c650aced17c30d57484a0df8040834416d4a69b9e32d816e3274affa8ad9c0753a270907
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.1] - 2026-05-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`Tablecop/SafeEndlessMethod` no longer corrupts methods whose body
|
|
13
|
+
contains a multi-statement `begin … end` nested as a sub-expression**
|
|
14
|
+
(e.g. the memoization idiom `@memo ||= begin; a; b; end`). The body
|
|
15
|
+
is an `or_asgn`, so the existing direct-body `:begin` guard missed
|
|
16
|
+
it, and it is not a `{}`/`do-end` block so the block guard missed it
|
|
17
|
+
too; autocorrect collapsed `a\n b` into `a b` (no separator) →
|
|
18
|
+
syntactically invalid Ruby. Found when it broke a downstream project
|
|
19
|
+
at parse time. Single-statement `begin x end` still converts.
|
|
20
|
+
|
|
21
|
+
> Note: 0.3.0 was released without a changelog entry; entries resume
|
|
22
|
+
> here rather than reconstruct unrecorded history.
|
|
23
|
+
|
|
8
24
|
## [0.2.0] - 2025-12-13
|
|
9
25
|
|
|
10
26
|
### Changed
|
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
|
|
@@ -66,6 +66,16 @@ module RuboCop
|
|
|
66
66
|
# Body must be single expression (not begin block with multiple children)
|
|
67
67
|
return false if node.body.begin_type?
|
|
68
68
|
|
|
69
|
+
# ...and must not CONTAIN a multi-statement sequence anywhere
|
|
70
|
+
# (e.g. `@memo ||= begin; a; b; end` — body is an or_asgn, so
|
|
71
|
+
# the check above misses it, and it is not a {}/do-end block so
|
|
72
|
+
# contains_multi_statement_block? misses it too). Collapsing its
|
|
73
|
+
# newlines to spaces would jam `a b` with no separator → invalid
|
|
74
|
+
# Ruby. This was a real corruption: rubocop-tablecop#<safe-
|
|
75
|
+
# endless> turned a valid memoized method into an unparseable
|
|
76
|
+
# one in a downstream project.
|
|
77
|
+
return false if contains_multi_statement_sequence?(node.body)
|
|
78
|
+
|
|
69
79
|
# No heredocs
|
|
70
80
|
return false if contains_heredoc?(node.body)
|
|
71
81
|
|
|
@@ -151,6 +161,19 @@ module RuboCop
|
|
|
151
161
|
false
|
|
152
162
|
end
|
|
153
163
|
|
|
164
|
+
# True if `body` is, or contains anywhere, a statement sequence
|
|
165
|
+
# with more than one statement. Two AST shapes (verified, not
|
|
166
|
+
# assumed): an implicit multi-statement body is a `:begin`
|
|
167
|
+
# sequence; an explicit `begin … end` is a `:kwbegin` whose
|
|
168
|
+
# children ARE the statements (no inner `:begin`). Either with
|
|
169
|
+
# >1 child is unsafe to space-join. Single-statement `begin x
|
|
170
|
+
# end` is a 1-child :kwbegin and stays convertible.
|
|
171
|
+
def contains_multi_statement_sequence?(body)
|
|
172
|
+
return false unless body
|
|
173
|
+
|
|
174
|
+
body.each_node(:begin, :kwbegin).any? { |seq| seq.children.size > 1 }
|
|
175
|
+
end
|
|
176
|
+
|
|
154
177
|
def contains_complex_control_flow?(node)
|
|
155
178
|
return false unless node
|
|
156
179
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joseph Wecker
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rubocop
|
|
@@ -53,6 +52,7 @@ files:
|
|
|
53
52
|
- lib/rubocop-tablecop.rb
|
|
54
53
|
- lib/rubocop/cop/tablecop/align_assignments.rb
|
|
55
54
|
- lib/rubocop/cop/tablecop/align_methods.rb
|
|
55
|
+
- lib/rubocop/cop/tablecop/condense_in.rb
|
|
56
56
|
- lib/rubocop/cop/tablecop/condense_when.rb
|
|
57
57
|
- lib/rubocop/cop/tablecop/safe_endless_method.rb
|
|
58
58
|
- lib/rubocop/cop/tablecop_cops.rb
|
|
@@ -67,7 +67,6 @@ metadata:
|
|
|
67
67
|
changelog_uri: https://github.com/v2-io/rubocop-tablecop/blob/main/CHANGELOG.md
|
|
68
68
|
rubygems_mfa_required: 'true'
|
|
69
69
|
default_lint_roller_plugin: RuboCop::Tablecop::Plugin
|
|
70
|
-
post_install_message:
|
|
71
70
|
rdoc_options: []
|
|
72
71
|
require_paths:
|
|
73
72
|
- lib
|
|
@@ -82,8 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
81
|
- !ruby/object:Gem::Version
|
|
83
82
|
version: '0'
|
|
84
83
|
requirements: []
|
|
85
|
-
rubygems_version:
|
|
86
|
-
signing_key:
|
|
84
|
+
rubygems_version: 4.0.3
|
|
87
85
|
specification_version: 4
|
|
88
86
|
summary: RuboCop extension for table-like, condensed Ruby formatting
|
|
89
87
|
test_files: []
|