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,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Tablecop
|
|
6
|
+
# Converts multi-line single-expression methods to single-line form.
|
|
7
|
+
#
|
|
8
|
+
# Uses endless method syntax (`def foo = expr`) for simple cases, but falls
|
|
9
|
+
# back to traditional one-liner (`def foo() expr end`) for methods with
|
|
10
|
+
# modifier-if/unless that call other methods (which can fail in module_eval
|
|
11
|
+
# contexts or at parse time).
|
|
12
|
+
#
|
|
13
|
+
# This cop avoids the bugs in RuboCop's Style/EndlessMethod:
|
|
14
|
+
# - Heredoc destruction
|
|
15
|
+
# - Rescue clause orphaning
|
|
16
|
+
# - module_eval context failures
|
|
17
|
+
# - Modifier-if with dynamic method failures
|
|
18
|
+
#
|
|
19
|
+
# @example Endless method (safe)
|
|
20
|
+
# # bad
|
|
21
|
+
# def foo
|
|
22
|
+
# 42
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
# def foo = 42
|
|
27
|
+
#
|
|
28
|
+
# @example Traditional one-liner (modifier-if with method call)
|
|
29
|
+
# # bad
|
|
30
|
+
# def clear!
|
|
31
|
+
# data_layer.clear! if data_layer.respond_to?(:clear!)
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# def clear!() data_layer.clear! if data_layer.respond_to?(:clear!) end
|
|
36
|
+
#
|
|
37
|
+
class SafeEndlessMethod < Base
|
|
38
|
+
extend AutoCorrector
|
|
39
|
+
|
|
40
|
+
MSG_ENDLESS = "Use endless method: `def %<name>s%<args>s = %<body>s`"
|
|
41
|
+
MSG_TRADITIONAL = "Use single-line method: `def %<name>s%<args>s %<body>s end`"
|
|
42
|
+
|
|
43
|
+
def on_def(node)
|
|
44
|
+
return unless convertible?(node)
|
|
45
|
+
|
|
46
|
+
if should_use_traditional?(node)
|
|
47
|
+
register_traditional_offense(node)
|
|
48
|
+
else
|
|
49
|
+
register_endless_offense(node)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
alias on_defs on_def
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def convertible?(node)
|
|
57
|
+
# Skip if already single-line
|
|
58
|
+
return false if node.single_line?
|
|
59
|
+
|
|
60
|
+
# Must have a body
|
|
61
|
+
return false unless node.body
|
|
62
|
+
|
|
63
|
+
# Setter methods (ending with =) can't use endless syntax
|
|
64
|
+
return false if node.method_name.to_s.end_with?("=")
|
|
65
|
+
|
|
66
|
+
# Body must be single expression (not begin block with multiple children)
|
|
67
|
+
return false if node.body.begin_type?
|
|
68
|
+
|
|
69
|
+
# No heredocs
|
|
70
|
+
return false if contains_heredoc?(node.body)
|
|
71
|
+
|
|
72
|
+
# No rescue/ensure (these are resbody/ensure node types in the method)
|
|
73
|
+
return false if has_rescue_or_ensure?(node)
|
|
74
|
+
|
|
75
|
+
# No multi-statement blocks - condensing them breaks syntax
|
|
76
|
+
# (statements need semicolons or newlines, not just spaces)
|
|
77
|
+
return false if contains_multi_statement_block?(node.body)
|
|
78
|
+
|
|
79
|
+
# No complex control flow (if/else, case) - can't condense safely
|
|
80
|
+
return false if contains_complex_control_flow?(node.body)
|
|
81
|
+
|
|
82
|
+
# Check line length
|
|
83
|
+
return false unless fits_line_length?(node)
|
|
84
|
+
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def should_use_traditional?(node)
|
|
89
|
+
# Use traditional one-liner if body has modifier-if/unless with method calls
|
|
90
|
+
# These can fail in module_eval contexts or at parse time
|
|
91
|
+
body = node.body
|
|
92
|
+
|
|
93
|
+
return false unless body.respond_to?(:type)
|
|
94
|
+
|
|
95
|
+
if body.type == :if && body.modifier_form?
|
|
96
|
+
# Check if the condition or body contains method calls
|
|
97
|
+
has_method_calls?(body)
|
|
98
|
+
else
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def has_method_calls?(node)
|
|
104
|
+
return false unless node
|
|
105
|
+
|
|
106
|
+
# Direct method call (send node)
|
|
107
|
+
return true if node.send_type?
|
|
108
|
+
|
|
109
|
+
# Check children
|
|
110
|
+
node.each_child_node do |child|
|
|
111
|
+
return true if has_method_calls?(child)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def contains_heredoc?(node)
|
|
118
|
+
return false unless node
|
|
119
|
+
|
|
120
|
+
return true if node.respond_to?(:heredoc?) && node.heredoc?
|
|
121
|
+
|
|
122
|
+
node.each_descendant(:str, :dstr, :xstr).any?(&:heredoc?)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def has_rescue_or_ensure?(node)
|
|
126
|
+
# Check if the method has a rescue or ensure block
|
|
127
|
+
# These show up as the method body being a :rescue or :ensure node
|
|
128
|
+
return true if node.body&.rescue_type?
|
|
129
|
+
return true if node.body&.ensure_type?
|
|
130
|
+
|
|
131
|
+
# Also check for rescue as part of method definition (def foo; rescue; end)
|
|
132
|
+
node.each_descendant(:rescue, :ensure, :resbody).any?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def contains_multi_statement_block?(node)
|
|
136
|
+
return false unless node
|
|
137
|
+
|
|
138
|
+
# Check if node itself is a block with multiple statements
|
|
139
|
+
if node.block_type? || node.numblock_type?
|
|
140
|
+
block_body = node.body
|
|
141
|
+
# Multiple statements show up as a :begin node
|
|
142
|
+
return true if block_body&.begin_type?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Check descendants for blocks with multiple statements
|
|
146
|
+
node.each_descendant(:block, :numblock) do |block_node|
|
|
147
|
+
block_body = block_node.body
|
|
148
|
+
return true if block_body&.begin_type?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def contains_complex_control_flow?(node)
|
|
155
|
+
return false unless node
|
|
156
|
+
|
|
157
|
+
# Multi-line if/unless/case can't be condensed safely
|
|
158
|
+
# (modifier-if is handled separately in should_use_traditional?)
|
|
159
|
+
if node.if_type? || node.case_type?
|
|
160
|
+
return true unless node.single_line?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check descendants for complex control flow
|
|
164
|
+
node.each_descendant(:if, :case) do |control_node|
|
|
165
|
+
return true unless control_node.single_line?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def fits_line_length?(node)
|
|
172
|
+
endless_line = build_endless_line(node)
|
|
173
|
+
traditional_line = build_traditional_line(node)
|
|
174
|
+
|
|
175
|
+
# Use the shorter form for checking
|
|
176
|
+
min_length = [endless_line.length, traditional_line.length].min
|
|
177
|
+
indent = node.loc.keyword.column
|
|
178
|
+
|
|
179
|
+
(indent + min_length) <= max_line_length
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_method_signature(node)
|
|
183
|
+
if node.defs_type?
|
|
184
|
+
# Singleton method: def self.foo or def obj.foo
|
|
185
|
+
receiver = node.receiver.source
|
|
186
|
+
"def #{receiver}.#{node.method_name}"
|
|
187
|
+
else
|
|
188
|
+
"def #{node.method_name}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_args(node)
|
|
193
|
+
return "" unless node.arguments?
|
|
194
|
+
|
|
195
|
+
args_source = node.arguments.source
|
|
196
|
+
# Arguments source may or may not include parens
|
|
197
|
+
if args_source.start_with?("(")
|
|
198
|
+
args_source
|
|
199
|
+
else
|
|
200
|
+
"(#{args_source})"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def build_endless_line(node)
|
|
205
|
+
sig = build_method_signature(node)
|
|
206
|
+
args = build_args(node)
|
|
207
|
+
body = node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
208
|
+
|
|
209
|
+
"#{sig}#{args} = #{body}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_traditional_line(node)
|
|
213
|
+
sig = build_method_signature(node)
|
|
214
|
+
args = if node.arguments?
|
|
215
|
+
args_source = node.arguments.source
|
|
216
|
+
args_source.start_with?("(") ? args_source : "(#{args_source})"
|
|
217
|
+
else
|
|
218
|
+
"()"
|
|
219
|
+
end
|
|
220
|
+
body = node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
221
|
+
|
|
222
|
+
"#{sig}#{args} #{body} end"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def register_endless_offense(node)
|
|
226
|
+
sig = build_method_signature(node)
|
|
227
|
+
args = build_args(node)
|
|
228
|
+
body = node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
229
|
+
|
|
230
|
+
message = format(MSG_ENDLESS, name: sig.sub("def ", ""), args: args, body: body)
|
|
231
|
+
|
|
232
|
+
add_offense(node, message: message) do |corrector|
|
|
233
|
+
corrector.replace(node, build_endless_line(node))
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def register_traditional_offense(node)
|
|
238
|
+
sig = build_method_signature(node)
|
|
239
|
+
args = node.arguments? ? "(#{node.arguments.source})" : "()"
|
|
240
|
+
body = node.body.source.gsub(/\s*\n\s*/, " ").strip
|
|
241
|
+
|
|
242
|
+
message = format(MSG_TRADITIONAL, name: sig.sub("def ", ""), args: args, body: body)
|
|
243
|
+
|
|
244
|
+
add_offense(node, message: message) do |corrector|
|
|
245
|
+
corrector.replace(node, build_traditional_line(node))
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Tablecop
|
|
7
|
+
# Integrates rubocop-tablecop with RuboCop's plugin system.
|
|
8
|
+
#
|
|
9
|
+
# This plugin provides cops for table-like, condensed Ruby formatting:
|
|
10
|
+
# - AlignAssignments: Align consecutive assignments on the = operator
|
|
11
|
+
# - AlignMethods: Align contiguous single-line method definitions
|
|
12
|
+
# - CondenseWhen: Condense multi-line when clauses to single lines
|
|
13
|
+
# - SafeEndlessMethod: Convert methods to endless form safely
|
|
14
|
+
class Plugin < LintRoller::Plugin
|
|
15
|
+
def about
|
|
16
|
+
LintRoller::About.new(
|
|
17
|
+
name: "rubocop-tablecop",
|
|
18
|
+
version: Version::STRING,
|
|
19
|
+
homepage: "https://github.com/v2-io/tablecop",
|
|
20
|
+
description: "Table-like, condensed Ruby formatting cops."
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def supported?(context)
|
|
25
|
+
context.engine == :rubocop
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rules(_context)
|
|
29
|
+
LintRoller::Rules.new(
|
|
30
|
+
type: :path,
|
|
31
|
+
config_format: :rubocop,
|
|
32
|
+
value: Pathname.new(__dir__).join("../../../config/default.yml")
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Tablecop
|
|
5
|
+
# Version information for rubocop-tablecop.
|
|
6
|
+
module Version
|
|
7
|
+
STRING = "0.2.0"
|
|
8
|
+
|
|
9
|
+
def self.document_version
|
|
10
|
+
STRING.match('\d+\.\d+').to_s
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# For backwards compatibility with code expecting RuboCop::Tablecop::VERSION
|
|
15
|
+
VERSION = Version::STRING
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubocop-tablecop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Joseph Wecker
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rubocop
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.72'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.72'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: lint_roller
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.1'
|
|
41
|
+
description: Custom RuboCop cops that enforce vertical alignment and condensed single-line
|
|
42
|
+
expressions where appropriate.
|
|
43
|
+
email:
|
|
44
|
+
- joseph@v2.io
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- config/default.yml
|
|
53
|
+
- lib/rubocop-tablecop.rb
|
|
54
|
+
- lib/rubocop/cop/tablecop/align_assignments.rb
|
|
55
|
+
- lib/rubocop/cop/tablecop/align_methods.rb
|
|
56
|
+
- lib/rubocop/cop/tablecop/condense_when.rb
|
|
57
|
+
- lib/rubocop/cop/tablecop/safe_endless_method.rb
|
|
58
|
+
- lib/rubocop/cop/tablecop_cops.rb
|
|
59
|
+
- lib/rubocop/tablecop.rb
|
|
60
|
+
- lib/rubocop/tablecop/plugin.rb
|
|
61
|
+
- lib/rubocop/tablecop/version.rb
|
|
62
|
+
homepage: https://github.com/v2-io/rubocop-tablecop
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
source_code_uri: https://github.com/v2-io/rubocop-tablecop
|
|
67
|
+
changelog_uri: https://github.com/v2-io/rubocop-tablecop/blob/main/CHANGELOG.md
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
default_lint_roller_plugin: RuboCop::Tablecop::Plugin
|
|
70
|
+
post_install_message:
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '3.1'
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.5.22
|
|
86
|
+
signing_key:
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: RuboCop extension for table-like, condensed Ruby formatting
|
|
89
|
+
test_files: []
|