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.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tablecop/align_assignments"
4
+ require_relative "tablecop/align_methods"
5
+ require_relative "tablecop/condense_when"
6
+ require_relative "tablecop/safe_endless_method"
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # RuboCop Tablecop project namespace.
5
+ # Provides cops for table-like, condensed Ruby formatting.
6
+ module Tablecop
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ require_relative "rubocop/tablecop"
6
+ require_relative "rubocop/tablecop/version"
7
+ require_relative "rubocop/tablecop/plugin"
8
+ require_relative "rubocop/cop/tablecop_cops"
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: []