ruby-lsp-refactor 0.1.0 → 0.1.2
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/.rubocop_todo.yml +68 -0
- data/README.md +553 -115
- data/lib/ruby/lsp/refactor/version.rb +1 -1
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +54 -17
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/accessor_listener.rb +156 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb +2 -2
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb +14 -14
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/constant_listener.rb +118 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb +105 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/enumerable_listener.rb +90 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb +172 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb +138 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb +7 -7
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/logical_operator_listener.rb +70 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +37 -109
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/raise_listener.rb +70 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rescue_listener.rb +85 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rspec_let_listener.rb +61 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_array_listener.rb +88 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_freeze_listener.rb +86 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb +2 -2
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb +95 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb +133 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +7 -49
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +62 -12
- data/lib/ruby_lsp/test_helper.rb +5 -5
- data/test/ruby_lsp_refactor/accessor_listener_test.rb +91 -0
- data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
- data/test/ruby_lsp_refactor/early_return_listener_test.rb +156 -0
- data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
- data/test/ruby_lsp_refactor/extract_include_file_listener_test.rb +189 -0
- data/test/ruby_lsp_refactor/extract_predicate_listener_test.rb +113 -0
- data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
- data/test/ruby_lsp_refactor/method_listener_test.rb +3 -52
- data/test/ruby_lsp_refactor/raise_listener_test.rb +64 -0
- data/test/ruby_lsp_refactor/rescue_listener_test.rb +72 -0
- data/test/ruby_lsp_refactor/rspec_let_listener_test.rb +54 -0
- data/test/ruby_lsp_refactor/string_array_listener_test.rb +64 -0
- data/test/ruby_lsp_refactor/string_freeze_listener_test.rb +52 -0
- data/test/ruby_lsp_refactor/string_listener_test.rb +2 -2
- data/test/ruby_lsp_refactor/super_listener_test.rb +65 -0
- data/test/ruby_lsp_refactor/tap_listener_test.rb +144 -0
- data/test/ruby_lsp_refactor/variable_listener_test.rb +0 -23
- metadata +42 -13
data/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# ruby-lsp-refactor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Beta software.** This gem is under active development. Refactorings may
|
|
4
|
+
> produce incorrect output in edge cases. A significant portion of the
|
|
5
|
+
> implementation was written with AI assistance — please review generated edits
|
|
6
|
+
> before committing them. Bug reports and corrections are very welcome.
|
|
7
|
+
|
|
8
|
+
A [ruby-lsp](https://github.com/Shopify/ruby-lsp) add-on that provides
|
|
4
9
|
AST-driven refactoring code actions natively inside any LSP-supported editor
|
|
5
10
|
(VS Code, Zed, Neovim, RubyMine, etc.).
|
|
6
11
|
|
|
@@ -27,12 +32,18 @@ bundle install
|
|
|
27
32
|
The add-on is discovered and activated automatically by ruby-lsp — no further
|
|
28
33
|
configuration is required.
|
|
29
34
|
|
|
35
|
+
> **Note on upstream overlap.** ruby-lsp already provides "Refactor: Extract
|
|
36
|
+
> Variable", "Refactor: Extract Method", and "Refactor: Toggle block style"
|
|
37
|
+
> natively. This add-on intentionally does not duplicate those actions — place
|
|
38
|
+
> your cursor on any expression or block and they will appear alongside the
|
|
39
|
+
> refactorings listed below.
|
|
40
|
+
|
|
30
41
|
## Supported refactorings
|
|
31
42
|
|
|
32
43
|
Place your cursor anywhere on the relevant construct and open the code-actions
|
|
33
44
|
menu (`Cmd+.` in VS Code / Zed, or your editor's equivalent).
|
|
34
45
|
|
|
35
|
-
###
|
|
46
|
+
### Conditionals
|
|
36
47
|
|
|
37
48
|
#### Convert to post-conditional
|
|
38
49
|
|
|
@@ -48,21 +59,9 @@ end
|
|
|
48
59
|
user.approve! if user.qualified?
|
|
49
60
|
```
|
|
50
61
|
|
|
51
|
-
Works with `unless` too:
|
|
52
|
-
|
|
53
|
-
```ruby
|
|
54
|
-
# Before
|
|
55
|
-
unless user.banned?
|
|
56
|
-
user.login!
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# After
|
|
60
|
-
user.login! unless user.banned?
|
|
61
|
-
```
|
|
62
|
-
|
|
63
62
|
#### Convert to block if / Convert to block unless
|
|
64
63
|
|
|
65
|
-
The reverse
|
|
64
|
+
The reverse — expands a trailing modifier back into a full block.
|
|
66
65
|
|
|
67
66
|
```ruby
|
|
68
67
|
# Before
|
|
@@ -76,24 +75,12 @@ end
|
|
|
76
75
|
|
|
77
76
|
#### Convert to unless / Convert to if
|
|
78
77
|
|
|
79
|
-
Toggles between `if` and `unless` on a block conditional
|
|
80
|
-
|
|
81
|
-
automatically
|
|
78
|
+
Toggles between `if` and `unless` on a block conditional with no `else` branch.
|
|
79
|
+
When the predicate already starts with `!`, the negation is stripped
|
|
80
|
+
automatically.
|
|
82
81
|
|
|
83
82
|
```ruby
|
|
84
83
|
# Before
|
|
85
|
-
if user.active?
|
|
86
|
-
user.greet!
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# After
|
|
90
|
-
unless user.active?
|
|
91
|
-
user.greet!
|
|
92
|
-
end
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
# Before — negated predicate
|
|
97
84
|
if !user.banned?
|
|
98
85
|
user.login!
|
|
99
86
|
end
|
|
@@ -106,8 +93,8 @@ end
|
|
|
106
93
|
|
|
107
94
|
#### Invert if/else
|
|
108
95
|
|
|
109
|
-
Negates the condition and swaps the two branches
|
|
110
|
-
|
|
96
|
+
Negates the condition and swaps the two branches. Double-negation (`!!`) is
|
|
97
|
+
cancelled automatically.
|
|
111
98
|
|
|
112
99
|
```ruby
|
|
113
100
|
# Before
|
|
@@ -125,164 +112,311 @@ else
|
|
|
125
112
|
end
|
|
126
113
|
```
|
|
127
114
|
|
|
128
|
-
#### Convert to
|
|
115
|
+
#### Convert to early return
|
|
129
116
|
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
Converts a guard `if` block at the top of a method into a `return unless`
|
|
118
|
+
statement, eliminating unnecessary nesting. The method body must have no
|
|
119
|
+
`else` branch and the `if` must be the first statement.
|
|
132
120
|
|
|
133
121
|
```ruby
|
|
134
|
-
# Before
|
|
135
|
-
|
|
122
|
+
# Before — cursor on the if
|
|
123
|
+
def charge_purchase(order)
|
|
124
|
+
if order.fulfilled?
|
|
125
|
+
OrderChargeConfirmation.new(order).create!
|
|
126
|
+
end
|
|
127
|
+
end
|
|
136
128
|
|
|
137
129
|
# After
|
|
138
|
-
|
|
130
|
+
def charge_purchase(order)
|
|
131
|
+
return unless order.fulfilled?
|
|
132
|
+
OrderChargeConfirmation.new(order).create!
|
|
133
|
+
end
|
|
139
134
|
```
|
|
140
135
|
|
|
141
136
|
---
|
|
142
137
|
|
|
143
|
-
###
|
|
138
|
+
### Strings
|
|
144
139
|
|
|
145
|
-
####
|
|
140
|
+
#### Convert to interpolated string
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
Upgrades a single-quoted string to double-quotes so you can immediately add
|
|
143
|
+
`#{}` interpolation. Embedded `"` characters are escaped.
|
|
149
144
|
|
|
150
145
|
```ruby
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
puts result
|
|
154
|
-
log result
|
|
146
|
+
'hello world' → "hello world"
|
|
147
|
+
```
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
#### Convert to string array / Convert to bracket array
|
|
150
|
+
|
|
151
|
+
Converts between a bracket array of plain strings and `%w[]` syntax.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
["foo", "bar", "baz"] → %w[foo bar baz]
|
|
155
|
+
%w[foo bar baz] → ["foo", "bar", "baz"]
|
|
159
156
|
```
|
|
160
157
|
|
|
161
|
-
####
|
|
158
|
+
#### Wrap in freeze / Remove freeze
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
inserted on the line above.
|
|
160
|
+
Adds or removes `.freeze` on a string literal.
|
|
165
161
|
|
|
166
162
|
```ruby
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
"hello" → "hello".freeze
|
|
164
|
+
"hello".freeze → "hello"
|
|
165
|
+
```
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Collections
|
|
170
|
+
|
|
171
|
+
#### Convert to symbol array
|
|
172
|
+
|
|
173
|
+
Converts a bracket array of plain symbols into a `%i[]` word array.
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
[:foo, :bar, :baz] → %i[foo bar baz]
|
|
173
177
|
```
|
|
174
178
|
|
|
175
179
|
#### Convert to keyword syntax
|
|
176
180
|
|
|
177
181
|
Converts hash-rocket pairs whose keys are plain symbols into modern keyword
|
|
178
|
-
syntax. Mixed hashes
|
|
179
|
-
|
|
182
|
+
syntax. Mixed hashes are handled gracefully — only eligible pairs are
|
|
183
|
+
converted.
|
|
180
184
|
|
|
181
185
|
```ruby
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
{ :name => "Alice", :age => 30 } → { name: "Alice", age: 30 }
|
|
187
|
+
```
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
#### Convert to .flat_map
|
|
190
|
+
|
|
191
|
+
Collapses a `map` + `flatten` / `flatten(1)` chain.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
items.map { |i| i.tags }.flatten(1) → items.flat_map { |i| i.tags }
|
|
187
195
|
```
|
|
188
196
|
|
|
189
|
-
#### Convert to
|
|
197
|
+
#### Convert to .find
|
|
190
198
|
|
|
191
|
-
|
|
199
|
+
Collapses a `select` + `first` chain.
|
|
192
200
|
|
|
193
201
|
```ruby
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
users.select { |u| u.admin? }.first → users.find { |u| u.admin? }
|
|
203
|
+
```
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
205
|
+
#### Convert to .filter_map
|
|
206
|
+
|
|
207
|
+
Collapses a `map` + `compact` chain.
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
items.map { |i| i.value }.compact → items.filter_map { |i| i.value }
|
|
199
211
|
```
|
|
200
212
|
|
|
201
213
|
---
|
|
202
214
|
|
|
203
|
-
###
|
|
215
|
+
### Variables & constants
|
|
204
216
|
|
|
205
|
-
####
|
|
217
|
+
#### Inline variable
|
|
206
218
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
inside the expression are automatically detected and forwarded as method
|
|
210
|
-
parameters.
|
|
219
|
+
Removes a local variable assignment and replaces every subsequent read with the
|
|
220
|
+
original right-hand-side expression.
|
|
211
221
|
|
|
212
222
|
```ruby
|
|
213
223
|
# Before — cursor on the assignment
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
result
|
|
218
|
-
end
|
|
224
|
+
result = user.calculate
|
|
225
|
+
puts result
|
|
226
|
+
log result
|
|
219
227
|
|
|
220
228
|
# After
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
puts user.calculate
|
|
230
|
+
log user.calculate
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Extract constant
|
|
234
|
+
|
|
235
|
+
Extracts a literal value (integer, float, string, symbol) inside a class or
|
|
236
|
+
module into a named constant at the top of the enclosing body.
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# Before — cursor on 100
|
|
240
|
+
class Processor
|
|
241
|
+
def run
|
|
242
|
+
items.first(100)
|
|
243
|
+
end
|
|
225
244
|
end
|
|
226
245
|
|
|
227
|
-
|
|
246
|
+
# After
|
|
247
|
+
class Processor
|
|
248
|
+
EXTRACTED_CONSTANT = 100
|
|
228
249
|
|
|
229
|
-
def
|
|
230
|
-
|
|
250
|
+
def run
|
|
251
|
+
items.first(EXTRACTED_CONSTANT)
|
|
231
252
|
end
|
|
253
|
+
end
|
|
232
254
|
```
|
|
233
255
|
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Methods & classes
|
|
259
|
+
|
|
234
260
|
#### Add parameter
|
|
235
261
|
|
|
236
|
-
Appends a `new_param` placeholder to a method's parameter list.
|
|
237
|
-
|
|
262
|
+
Appends a `new_param` placeholder to a method's parameter list. Parentheses
|
|
263
|
+
are added automatically when the method has none.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
def greet(name) → def greet(name, new_param)
|
|
267
|
+
def greet → def greet(new_param)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Convert to keyword arguments
|
|
271
|
+
|
|
272
|
+
Rewrites required positional parameters to keyword arguments. Optional
|
|
273
|
+
parameters, rest args, and block parameters are left unchanged.
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
def create(name, age) → def create(name:, age:)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Convert to attr_accessor
|
|
280
|
+
|
|
281
|
+
Detects an `attr_reader` paired with a canonical manual writer
|
|
282
|
+
(`def name=(val); @name = val; end`) and collapses them into a single
|
|
283
|
+
`attr_accessor`.
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
# Before — cursor on either line
|
|
287
|
+
attr_reader :name
|
|
288
|
+
def name=(val)
|
|
289
|
+
@name = val
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# After
|
|
293
|
+
attr_accessor :name
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Wrap body in rescue
|
|
297
|
+
|
|
298
|
+
Wraps a method's entire body in a `rescue StandardError => e` clause with a
|
|
299
|
+
`raise` placeholder so you can fill in the error handling without accidentally
|
|
300
|
+
swallowing exceptions.
|
|
238
301
|
|
|
239
302
|
```ruby
|
|
240
|
-
# Before
|
|
241
|
-
def
|
|
242
|
-
|
|
303
|
+
# Before
|
|
304
|
+
def call
|
|
305
|
+
do_thing
|
|
243
306
|
end
|
|
244
307
|
|
|
245
308
|
# After
|
|
246
|
-
def
|
|
247
|
-
|
|
309
|
+
def call
|
|
310
|
+
do_thing
|
|
311
|
+
rescue StandardError => e
|
|
312
|
+
raise
|
|
248
313
|
end
|
|
249
314
|
```
|
|
250
315
|
|
|
316
|
+
#### Extract predicate methods
|
|
317
|
+
|
|
318
|
+
Extracts each operand of a compound `&&` or `||` expression that is the sole
|
|
319
|
+
statement in a method into its own private predicate method. The generated
|
|
320
|
+
names `predicate_1?` / `predicate_2?` are placeholders — rename them to
|
|
321
|
+
reflect intent.
|
|
322
|
+
|
|
251
323
|
```ruby
|
|
252
|
-
# Before —
|
|
253
|
-
def
|
|
254
|
-
|
|
324
|
+
# Before — cursor on the compound expression
|
|
325
|
+
def eligible_for_return?
|
|
326
|
+
expired_orders.exclude?(self) && self.value > MINIMUM_RETURN_VALUE
|
|
255
327
|
end
|
|
256
328
|
|
|
257
329
|
# After
|
|
258
|
-
def
|
|
259
|
-
|
|
330
|
+
def eligible_for_return?
|
|
331
|
+
predicate_1? && predicate_2?
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
private
|
|
335
|
+
|
|
336
|
+
def predicate_1?
|
|
337
|
+
expired_orders.exclude?(self)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def predicate_2?
|
|
341
|
+
self.value > MINIMUM_RETURN_VALUE
|
|
260
342
|
end
|
|
261
343
|
```
|
|
262
344
|
|
|
263
|
-
#### Convert to
|
|
345
|
+
#### Convert to explicit super
|
|
346
|
+
|
|
347
|
+
Converts a bare `super` (which forwards all arguments implicitly) into an
|
|
348
|
+
explicit `super(param1, param2, ...)` using the enclosing method's parameter
|
|
349
|
+
names.
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
def initialize(name, age)
|
|
353
|
+
super → super(name, age)
|
|
354
|
+
end
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### Operators & blocks
|
|
264
360
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
361
|
+
#### Convert to tap
|
|
362
|
+
|
|
363
|
+
Converts a sequence of method calls on the same receiver followed by a bare
|
|
364
|
+
return of that receiver into an `Object#tap` block, grouping the operations
|
|
365
|
+
and removing the explicit return.
|
|
268
366
|
|
|
269
367
|
```ruby
|
|
270
|
-
# Before — cursor anywhere
|
|
271
|
-
def
|
|
272
|
-
|
|
368
|
+
# Before — cursor anywhere in the method
|
|
369
|
+
def do_something
|
|
370
|
+
obj.do_first_thing
|
|
371
|
+
obj.do_second_thing
|
|
372
|
+
obj.do_third_thing
|
|
373
|
+
obj
|
|
273
374
|
end
|
|
274
375
|
|
|
275
376
|
# After
|
|
276
|
-
def
|
|
277
|
-
|
|
377
|
+
def do_something
|
|
378
|
+
obj.tap do |o|
|
|
379
|
+
o.do_first_thing
|
|
380
|
+
o.do_second_thing
|
|
381
|
+
o.do_third_thing
|
|
382
|
+
end
|
|
278
383
|
end
|
|
279
384
|
```
|
|
280
385
|
|
|
281
|
-
####
|
|
386
|
+
#### Convert `&&` to `and` / `and` to `&&`
|
|
387
|
+
|
|
388
|
+
Toggles between symbolic and word forms of the logical AND operator.
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
user.valid? && user.save → user.valid? and user.save
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Convert `||` to `or` / `or` to `||`
|
|
395
|
+
|
|
396
|
+
Toggles between symbolic and word forms of the logical OR operator.
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
a || b → a or b
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Simplify raise
|
|
282
403
|
|
|
283
|
-
|
|
284
|
-
`
|
|
285
|
-
|
|
404
|
+
Removes the redundant `RuntimeError` class from a two-argument `raise` or
|
|
405
|
+
`fail` call. `RuntimeError` is Ruby's default exception class and need not be
|
|
406
|
+
stated explicitly.
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
raise RuntimeError, "oops" → raise "oops"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### RSpec
|
|
415
|
+
|
|
416
|
+
#### Extract to let
|
|
417
|
+
|
|
418
|
+
Moves a local variable assignment inside an `it`/`specify`/`example`/`scenario`
|
|
419
|
+
block into a `let` declaration above the example.
|
|
286
420
|
|
|
287
421
|
```ruby
|
|
288
422
|
# Before — cursor on the assignment
|
|
@@ -299,14 +433,318 @@ it "logs in" do
|
|
|
299
433
|
end
|
|
300
434
|
```
|
|
301
435
|
|
|
436
|
+
#### Convert let to let! / let! to let
|
|
437
|
+
|
|
438
|
+
Toggles between lazy (`let`) and eager (`let!`) memoization.
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
let(:user) { User.new } → let!(:user) { User.new }
|
|
442
|
+
let!(:user) { User.new } → let(:user) { User.new }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Planned refactorings
|
|
448
|
+
|
|
449
|
+
The items below are on the roadmap but not yet implemented. They are tracked
|
|
450
|
+
here so the intent is not lost.
|
|
451
|
+
|
|
452
|
+
### Single-file (not yet implemented)
|
|
453
|
+
|
|
454
|
+
| Refactoring | Description |
|
|
455
|
+
|---|---|
|
|
456
|
+
| **Introduce field** | Extracts an expression inside a method into an instance variable (`@name`), inserting the assignment at the top of the method or into `initialize`. |
|
|
457
|
+
|
|
458
|
+
### Multi-file
|
|
459
|
+
|
|
460
|
+
These refactorings create new files and/or update call sites across the
|
|
461
|
+
project. The ones marked ✅ are already implemented using `document_changes`
|
|
462
|
+
in the `WorkspaceEdit` response, which lets a single code action atomically
|
|
463
|
+
create files and edit multiple documents. The ones marked 🔲 require
|
|
464
|
+
workspace-level index support or are pending implementation.
|
|
465
|
+
|
|
466
|
+
#### ✅ Extract Include File
|
|
467
|
+
|
|
468
|
+
Extracts a top-level `module` or `class` into its own file and replaces it
|
|
469
|
+
with a `require_relative` statement. Offered when the cursor is on a module or
|
|
470
|
+
class that coexists with other top-level statements in the same file.
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# Before — app/models/user.rb (cursor on the module)
|
|
474
|
+
module Greetable
|
|
475
|
+
def greet = "hello"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
class User
|
|
479
|
+
include Greetable
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# After — app/models/greetable.rb (new file, created automatically)
|
|
483
|
+
# frozen_string_literal: true
|
|
484
|
+
|
|
485
|
+
module Greetable
|
|
486
|
+
def greet = "hello"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# After — app/models/user.rb (modified)
|
|
490
|
+
require_relative "greetable"
|
|
491
|
+
|
|
492
|
+
class User
|
|
493
|
+
include Greetable
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### 🔲 Extract Service Object
|
|
498
|
+
|
|
499
|
+
Moves callback logic out of a controller into a dedicated service object file.
|
|
500
|
+
Addresses the Rails antipattern of using `after_action` callbacks for
|
|
501
|
+
operations that depend on the success of the triggering action.
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
# Before — app/controllers/users_controller.rb
|
|
505
|
+
class UsersController < ApplicationController
|
|
506
|
+
after_action :send_confirmation_email, only: [:create]
|
|
507
|
+
|
|
508
|
+
def create
|
|
509
|
+
@user = User.create!(user_params)
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# After — app/services/user_confirmation_service.rb (new file)
|
|
514
|
+
class UserConfirmationService
|
|
515
|
+
def initialize(user) = @user = user
|
|
516
|
+
|
|
517
|
+
def call
|
|
518
|
+
AccountCreationMailer.new(@user).deliver! if @user.persisted?
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# After — app/controllers/users_controller.rb (modified)
|
|
523
|
+
class UsersController < ApplicationController
|
|
524
|
+
def create
|
|
525
|
+
@user = User.create!(user_params)
|
|
526
|
+
UserConfirmationService.new(@user).call
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### 🔲 Extract Form Object
|
|
532
|
+
|
|
533
|
+
Extracts a model that uses `accepts_nested_attributes_for` into a plain Ruby
|
|
534
|
+
form object that includes `ActiveModel::Model`, making the form flat,
|
|
535
|
+
explicitly validated, and easy to test.
|
|
536
|
+
|
|
537
|
+
Creates a new file under `app/forms/` and updates the controller and view to
|
|
538
|
+
use the form object instead of the model directly.
|
|
539
|
+
|
|
540
|
+
#### 🔲 Extract Policy Class
|
|
541
|
+
|
|
542
|
+
When a method contains more than two or three compound conditions, extracts
|
|
543
|
+
them all into a dedicated policy class with individual predicate methods. Each
|
|
544
|
+
predicate becomes a public method on the policy, making them independently
|
|
545
|
+
testable without stubs.
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
# Before — single method with many conditions
|
|
549
|
+
def eligible_for_return?
|
|
550
|
+
not_expired? && over_minimum_value? && customer_not_fraudulent?
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# After — app/policies/return_eligibility_policy.rb (new file)
|
|
554
|
+
class ReturnEligibilityPolicy
|
|
555
|
+
def initialize(order) = @order = order
|
|
556
|
+
|
|
557
|
+
def eligible?
|
|
558
|
+
not_expired? && over_minimum_value? && customer_not_fraudulent?
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def not_expired? = Order.expired_orders.exclude?(@order)
|
|
562
|
+
def over_minimum_value? = @order.value > Order::MINIMUM_RETURN_VALUE
|
|
563
|
+
def customer_not_fraudulent? = @order.user.not_fraudulent?
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# After — calling code (modified)
|
|
567
|
+
def eligible_for_return?
|
|
568
|
+
ReturnEligibilityPolicy.new(self).eligible?
|
|
569
|
+
end
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### 🔲 Combine Functions into Class
|
|
573
|
+
|
|
574
|
+
When several methods in a file all take the same object as their first
|
|
575
|
+
argument, extracts them into a new class where that object becomes an injected
|
|
576
|
+
dependency. Implements the
|
|
577
|
+
[Combine Functions into Class](https://refactoring.com/catalog/combineFunctionsIntoClass.html)
|
|
578
|
+
pattern.
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
# Before — repeated argument is a smell
|
|
582
|
+
def format_name(user) = "#{user.first_name} #{user.last_name}"
|
|
583
|
+
def greeting(user) = "Hello, #{format_name(user)}"
|
|
584
|
+
def farewell(user) = "Goodbye, #{format_name(user)}"
|
|
585
|
+
|
|
586
|
+
# After — app/presenters/user_presenter.rb (new file)
|
|
587
|
+
class UserPresenter
|
|
588
|
+
def initialize(user) = @user = user
|
|
589
|
+
|
|
590
|
+
def format_name = "#{@user.first_name} #{@user.last_name}"
|
|
591
|
+
def greeting = "Hello, #{format_name}"
|
|
592
|
+
def farewell = "Goodbye, #{format_name}"
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### 🔲 Introduce Null Object
|
|
597
|
+
|
|
598
|
+
When a method guards against a nil association with an `if` check before
|
|
599
|
+
delegating to it, extracts a null object class that implements the same
|
|
600
|
+
interface with safe default behaviour, removing the conditional entirely.
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
# Before
|
|
604
|
+
if @user.has_address?
|
|
605
|
+
@user.address.street_name
|
|
606
|
+
else
|
|
607
|
+
"Unknown street"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# After — app/models/null_address.rb (new file)
|
|
611
|
+
class NullAddress
|
|
612
|
+
def street_name = "Unknown street"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# After — app/models/user.rb (modified)
|
|
616
|
+
class User
|
|
617
|
+
def address = @address || NullAddress.new
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# After — calling code (no conditional needed)
|
|
621
|
+
@user.address.street_name
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
#### 🔲 Rename
|
|
625
|
+
|
|
626
|
+
Renames a method, class, module, constant, or local variable and updates
|
|
627
|
+
every reference to it across the entire project. Requires the ruby-lsp index
|
|
628
|
+
to locate all usages safely.
|
|
629
|
+
|
|
630
|
+
#### 🔲 Extract Parameter
|
|
631
|
+
|
|
632
|
+
Extracts an expression inside a method body into a new parameter, adding it
|
|
633
|
+
to the method signature and updating every call site in the project to pass
|
|
634
|
+
the extracted value.
|
|
635
|
+
|
|
636
|
+
```ruby
|
|
637
|
+
# Before
|
|
638
|
+
def greet
|
|
639
|
+
"Hello, #{DEFAULT_NAME}"
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# After — signature and all call sites updated
|
|
643
|
+
def greet(name = DEFAULT_NAME)
|
|
644
|
+
"Hello, #{name}"
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
#### 🔲 Extract Superclass
|
|
649
|
+
|
|
650
|
+
Extracts selected methods from a class into a new superclass and makes the
|
|
651
|
+
original class inherit from it. Creates a new file for the superclass.
|
|
652
|
+
|
|
653
|
+
```ruby
|
|
654
|
+
# Before — app/models/animal.rb
|
|
655
|
+
class Animal
|
|
656
|
+
def breathe = "breathing"
|
|
657
|
+
def eat = "eating"
|
|
658
|
+
def speak = raise NotImplementedError
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# After — app/models/living_thing.rb (new file)
|
|
662
|
+
class LivingThing
|
|
663
|
+
def breathe = "breathing"
|
|
664
|
+
def eat = "eating"
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# After — app/models/animal.rb (modified)
|
|
668
|
+
class Animal < LivingThing
|
|
669
|
+
def speak = raise NotImplementedError
|
|
670
|
+
end
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### 🔲 Extract Module
|
|
674
|
+
|
|
675
|
+
Extracts selected methods from a class into a new module and adds an
|
|
676
|
+
`include` statement. Creates a new file for the module.
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
# Before
|
|
680
|
+
class Report
|
|
681
|
+
def format_header = "=== Report ==="
|
|
682
|
+
def format_footer = "=== End ==="
|
|
683
|
+
def generate = "#{format_header}\n...\n#{format_footer}"
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# After — app/concerns/formattable.rb (new file)
|
|
687
|
+
module Formattable
|
|
688
|
+
def format_header = "=== Report ==="
|
|
689
|
+
def format_footer = "=== End ==="
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# After — app/models/report.rb (modified)
|
|
693
|
+
class Report
|
|
694
|
+
include Formattable
|
|
695
|
+
def generate = "#{format_header}\n...\n#{format_footer}"
|
|
696
|
+
end
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### 🔲 Pull Members Up / Push Members Down
|
|
700
|
+
|
|
701
|
+
Moves methods between a class and its superclass. "Pull up" moves a method
|
|
702
|
+
from a subclass to the superclass; "push down" moves it from the superclass
|
|
703
|
+
into one or more subclasses. Both operations update all affected files.
|
|
704
|
+
|
|
705
|
+
#### 🔲 Safe Delete
|
|
706
|
+
|
|
707
|
+
Deletes a method, class, or constant only after verifying it has no usages
|
|
708
|
+
anywhere in the project. Requires the ruby-lsp index to confirm the symbol is
|
|
709
|
+
unreferenced before removing it.
|
|
710
|
+
|
|
711
|
+
#### 🔲 Extract Partial _(Rails)_
|
|
712
|
+
|
|
713
|
+
Extracts a fragment of an ERB view template into a new partial file and
|
|
714
|
+
replaces the original fragment with a `render` call.
|
|
715
|
+
|
|
716
|
+
```erb
|
|
717
|
+
<%# Before — app/views/users/show.html.erb %>
|
|
718
|
+
<div class="profile">
|
|
719
|
+
<h1><%= @user.name %></h1>
|
|
720
|
+
<p><%= @user.bio %></p>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<%# After — app/views/users/_profile.html.erb (new file) %>
|
|
724
|
+
<div class="profile">
|
|
725
|
+
<h1><%= user.name %></h1>
|
|
726
|
+
<p><%= user.bio %></p>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<%# After — app/views/users/show.html.erb (modified) %>
|
|
730
|
+
<%= render "profile", user: @user %>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
#### 🔲 Extract Include File (generic)
|
|
734
|
+
|
|
735
|
+
Extracts an arbitrary block of Ruby code (not necessarily a named module or
|
|
736
|
+
class) into a new file and replaces it with a `require_relative` statement.
|
|
737
|
+
The ✅ variant above handles the named module/class case automatically; this
|
|
738
|
+
generic form would handle any selected lines.
|
|
739
|
+
|
|
302
740
|
---
|
|
303
741
|
|
|
304
742
|
## Development
|
|
305
743
|
|
|
306
744
|
```bash
|
|
307
|
-
bin/setup
|
|
308
|
-
bundle exec rake test
|
|
309
|
-
bundle exec rake
|
|
745
|
+
bin/setup # install dependencies
|
|
746
|
+
bundle exec rake test # run the test suite
|
|
747
|
+
bundle exec rake # lint + test
|
|
310
748
|
```
|
|
311
749
|
|
|
312
750
|
To try the add-on against a local project without publishing to RubyGems, add
|