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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +68 -0
  3. data/README.md +553 -115
  4. data/lib/ruby/lsp/refactor/version.rb +1 -1
  5. data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +54 -17
  6. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/accessor_listener.rb +156 -0
  7. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb +2 -2
  8. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb +14 -14
  9. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/constant_listener.rb +118 -0
  10. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb +105 -0
  11. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/enumerable_listener.rb +90 -0
  12. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb +172 -0
  13. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb +138 -0
  14. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb +7 -7
  15. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/logical_operator_listener.rb +70 -0
  16. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +37 -109
  17. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/raise_listener.rb +70 -0
  18. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rescue_listener.rb +85 -0
  19. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rspec_let_listener.rb +61 -0
  20. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_array_listener.rb +88 -0
  21. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_freeze_listener.rb +86 -0
  22. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb +2 -2
  23. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb +95 -0
  24. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb +133 -0
  25. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +7 -49
  26. data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +62 -12
  27. data/lib/ruby_lsp/test_helper.rb +5 -5
  28. data/test/ruby_lsp_refactor/accessor_listener_test.rb +91 -0
  29. data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
  30. data/test/ruby_lsp_refactor/early_return_listener_test.rb +156 -0
  31. data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
  32. data/test/ruby_lsp_refactor/extract_include_file_listener_test.rb +189 -0
  33. data/test/ruby_lsp_refactor/extract_predicate_listener_test.rb +113 -0
  34. data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
  35. data/test/ruby_lsp_refactor/method_listener_test.rb +3 -52
  36. data/test/ruby_lsp_refactor/raise_listener_test.rb +64 -0
  37. data/test/ruby_lsp_refactor/rescue_listener_test.rb +72 -0
  38. data/test/ruby_lsp_refactor/rspec_let_listener_test.rb +54 -0
  39. data/test/ruby_lsp_refactor/string_array_listener_test.rb +64 -0
  40. data/test/ruby_lsp_refactor/string_freeze_listener_test.rb +52 -0
  41. data/test/ruby_lsp_refactor/string_listener_test.rb +2 -2
  42. data/test/ruby_lsp_refactor/super_listener_test.rb +65 -0
  43. data/test/ruby_lsp_refactor/tap_listener_test.rb +144 -0
  44. data/test/ruby_lsp_refactor/variable_listener_test.rb +0 -23
  45. metadata +42 -13
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # ruby-lsp-refactor
2
2
 
3
- A [ruby-lsp](https://github.com/Shopify/ruby-lsp) add-on that provides safe,
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
- ### Phase 1 — Local rewrites
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 operation — expands a trailing modifier back into a full block.
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 that has no `else`
80
- branch. When the predicate already starts with `!`, the negation is stripped
81
- automatically to keep the result clean.
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 of an `if/else` block.
110
- Double-negation (`!!`) is cancelled automatically.
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 interpolated string
115
+ #### Convert to early return
129
116
 
130
- Upgrades a single-quoted string literal to double-quotes so you can immediately
131
- add `#{}` interpolation. Any `"` characters inside the string are escaped.
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
- 'hello world'
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
- "hello world"
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
- ### Phase 2 — Variable & literal optimisation
138
+ ### Strings
144
139
 
145
- #### Inline variable
140
+ #### Convert to interpolated string
146
141
 
147
- Removes a local variable assignment and replaces every subsequent read of that
148
- variable with the original right-hand-side expression.
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
- # Before — cursor on the assignment line
152
- result = user.calculate
153
- puts result
154
- log result
146
+ 'hello world' → "hello world"
147
+ ```
155
148
 
156
- # After
157
- puts user.calculate
158
- log user.calculate
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
- #### Extract local variable
158
+ #### Wrap in freeze / Remove freeze
162
159
 
163
- Wraps any expression under the cursor in a new local variable assignment
164
- inserted on the line above.
160
+ Adds or removes `.freeze` on a string literal.
165
161
 
166
162
  ```ruby
167
- # Before — cursor on the expression
168
- user.full_name.upcase
163
+ "hello" → "hello".freeze
164
+ "hello".freeze → "hello"
165
+ ```
169
166
 
170
- # After
171
- variable = user.full_name.upcase
172
- variable
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 (string keys, computed keys) are handled gracefully
179
- only the eligible pairs are converted.
182
+ syntax. Mixed hashes are handled gracefully only eligible pairs are
183
+ converted.
180
184
 
181
185
  ```ruby
182
- # Before
183
- { :name => "Alice", :age => 30 }
186
+ { :name => "Alice", :age => 30 } → { name: "Alice", age: 30 }
187
+ ```
184
188
 
185
- # After
186
- { name: "Alice", age: 30 }
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 symbol array
197
+ #### Convert to .find
190
198
 
191
- Converts a bracket array of plain symbols into a `%i[]` word array.
199
+ Collapses a `select` + `first` chain.
192
200
 
193
201
  ```ruby
194
- # Before
195
- [:foo, :bar, :baz]
202
+ users.select { |u| u.admin? }.first → users.find { |u| u.admin? }
203
+ ```
196
204
 
197
- # After
198
- %i[foo bar baz]
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
- ### Phase 3 — Advanced structure
215
+ ### Variables & constants
204
216
 
205
- #### Extract to method
217
+ #### Inline variable
206
218
 
207
- Extracts a local variable's right-hand-side expression into a new `private`
208
- method. Variables that are defined before the extraction point and referenced
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
- def process(data)
215
- threshold = 10
216
- result = data.select { |x| x > threshold }
217
- result
218
- end
224
+ result = user.calculate
225
+ puts result
226
+ log result
219
227
 
220
228
  # After
221
- def process(data)
222
- threshold = 10
223
- result = result(threshold)
224
- result
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
- private
246
+ # After
247
+ class Processor
248
+ EXTRACTED_CONSTANT = 100
228
249
 
229
- def result(threshold)
230
- data.select { |x| x > threshold }
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. If the method
237
- has no parameters yet, parentheses are added automatically.
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 — cursor anywhere inside the def
241
- def greet(name)
242
- puts name
303
+ # Before
304
+ def call
305
+ do_thing
243
306
  end
244
307
 
245
308
  # After
246
- def greet(name, new_param)
247
- puts name
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 — no parameters
253
- def greet
254
- puts "hello"
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 greet(new_param)
259
- puts "hello"
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 keyword arguments
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
- Rewrites all required positional parameters in a method signature to keyword
266
- arguments. Optional parameters, rest args, and block parameters are left
267
- unchanged.
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 inside the def
271
- def create(name, age)
272
- User.new(name, age)
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 create(name:, age:)
277
- User.new(name, age)
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
- #### Extract to let _(RSpec)_
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
- When the cursor is on a local variable assignment inside an RSpec `it`,
284
- `specify`, `example`, or `scenario` block, this action moves the assignment
285
- into a `let` declaration inserted above the example.
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 # install dependencies
308
- bundle exec rake test # run the test suite
309
- bundle exec rake # lint + test
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