ruby-lsp-refactor 0.1.1 → 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.
@@ -3,7 +3,7 @@
3
3
  module Ruby
4
4
  module Lsp
5
5
  module Refactor
6
- VERSION = "0.1.1"
6
+ VERSION = "0.1.2"
7
7
  end
8
8
  end
9
9
  end
@@ -2,27 +2,40 @@
2
2
 
3
3
  require "ruby_lsp/addon"
4
4
 
5
- # Phase 1 – Local rewrites
5
+ # Conditionals
6
6
  require_relative "listeners/conditional_listener"
7
- require_relative "listeners/string_listener"
8
- require_relative "listeners/block_style_listener"
9
- require_relative "listeners/logical_operator_listener"
7
+ require_relative "listeners/early_return_listener"
10
8
 
11
- # Phase 2 – Variable & literal optimisation
12
- require_relative "listeners/variable_listener"
13
- require_relative "listeners/hash_listener"
14
- require_relative "listeners/array_listener"
9
+ # Strings
10
+ require_relative "listeners/string_listener"
15
11
  require_relative "listeners/string_array_listener"
16
12
  require_relative "listeners/string_freeze_listener"
13
+
14
+ # Collections
15
+ require_relative "listeners/array_listener"
16
+ require_relative "listeners/hash_listener"
17
17
  require_relative "listeners/enumerable_listener"
18
- require_relative "listeners/raise_listener"
19
18
 
20
- # Phase 3 – Advanced structure
21
- require_relative "listeners/method_listener"
19
+ # Variables & constants
20
+ require_relative "listeners/variable_listener"
22
21
  require_relative "listeners/constant_listener"
22
+
23
+ # Methods & classes
24
+ require_relative "listeners/method_listener"
25
+ require_relative "listeners/extract_predicate_listener"
23
26
  require_relative "listeners/accessor_listener"
24
27
  require_relative "listeners/rescue_listener"
25
28
  require_relative "listeners/super_listener"
29
+
30
+ # Multi-file
31
+ require_relative "listeners/extract_include_file_listener"
32
+
33
+ # Operators & blocks
34
+ require_relative "listeners/tap_listener"
35
+ require_relative "listeners/logical_operator_listener"
36
+ require_relative "listeners/raise_listener"
37
+
38
+ # RSpec
26
39
  require_relative "listeners/rspec_let_listener"
27
40
 
28
41
  module RubyLsp
@@ -90,27 +103,25 @@ module RubyLsp
90
103
  response_builder = RubyLsp::ResponseBuilders::CollectionResponseBuilder.new
91
104
  dispatcher = Prism::Dispatcher.new
92
105
 
93
- # Phase 1 – Local rewrites
94
106
  ConditionalListener.new(response_builder, node_context, dispatcher)
107
+ EarlyReturnListener.new(response_builder, node_context, dispatcher)
95
108
  StringListener.new(response_builder, node_context, dispatcher)
96
- BlockStyleListener.new(response_builder, node_context, dispatcher)
97
- LogicalOperatorListener.new(response_builder, node_context, dispatcher)
98
-
99
- # Phase 2 – Variable & literal optimisation
100
- VariableListener.new(response_builder, node_context, dispatcher)
101
- HashListener.new(response_builder, node_context, dispatcher)
102
- ArrayListener.new(response_builder, node_context, dispatcher)
103
109
  StringArrayListener.new(response_builder, node_context, dispatcher)
104
110
  StringFreezeListener.new(response_builder, node_context, dispatcher)
111
+ ArrayListener.new(response_builder, node_context, dispatcher)
112
+ HashListener.new(response_builder, node_context, dispatcher)
105
113
  EnumerableListener.new(response_builder, node_context, dispatcher)
106
- RaiseListener.new(response_builder, node_context, dispatcher)
107
-
108
- # Phase 3 – Advanced structure
109
- MethodListener.new(response_builder, node_context, dispatcher)
114
+ VariableListener.new(response_builder, node_context, dispatcher)
110
115
  ConstantListener.new(response_builder, node_context, dispatcher)
116
+ MethodListener.new(response_builder, node_context, dispatcher)
117
+ ExtractPredicateListener.new(response_builder, node_context, dispatcher)
111
118
  AccessorListener.new(response_builder, node_context, dispatcher)
112
119
  RescueListener.new(response_builder, node_context, dispatcher)
113
120
  SuperListener.new(response_builder, node_context, dispatcher)
121
+ ExtractIncludeFileListener.new(response_builder, node_context, dispatcher)
122
+ TapListener.new(response_builder, node_context, dispatcher)
123
+ LogicalOperatorListener.new(response_builder, node_context, dispatcher)
124
+ RaiseListener.new(response_builder, node_context, dispatcher)
114
125
  RspecLetListener.new(response_builder, node_context, dispatcher)
115
126
 
116
127
  dispatcher.dispatch(document.ast)
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/node_helpers"
4
+
5
+ module RubyLsp
6
+ module Refactor
7
+ # Offers "Convert to early return" when the cursor is on a guard `if` block
8
+ # that is the first statement in a method body and has no `else` branch.
9
+ #
10
+ # From "Prefer early returns" in Refactoring Rails: replace a top-level
11
+ # guard `if` with `return unless` so the happy path is not nested.
12
+ #
13
+ # Input (cursor on the `if` line):
14
+ # def charge_purchase(order)
15
+ # if order.fulfilled?
16
+ # OrderChargeConfirmation.new(order).create!
17
+ # end
18
+ # end
19
+ #
20
+ # Output:
21
+ # def charge_purchase(order)
22
+ # return unless order.fulfilled?
23
+ # OrderChargeConfirmation.new(order).create!
24
+ # end
25
+ #
26
+ # Eligibility:
27
+ # - Block-form `if` (has end_keyword_loc) with no else/elsif.
28
+ # - Must be the first statement in the enclosing method body.
29
+ # - Body may contain one or more statements.
30
+ class EarlyReturnListener
31
+ include RubyLsp::Requests::Support::Common
32
+ include Support::NodeHelpers
33
+
34
+ def initialize(response_builder, node_context, dispatcher)
35
+ @response_builder = response_builder
36
+ @node_context = node_context
37
+ @current_def = nil
38
+
39
+ dispatcher.register(
40
+ self,
41
+ :on_def_node_enter,
42
+ :on_def_node_leave,
43
+ :on_if_node_enter
44
+ )
45
+ end
46
+
47
+ def on_def_node_enter(node)
48
+ @current_def = node
49
+ rescue StandardError
50
+ nil
51
+ end
52
+
53
+ def on_def_node_leave(_node)
54
+ @current_def = nil
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ def on_if_node_enter(node)
60
+ return unless node_covers_cursor?(node)
61
+ return unless eligible?(node)
62
+
63
+ emit_early_return(node)
64
+ rescue StandardError
65
+ nil
66
+ end
67
+
68
+ private
69
+
70
+ def eligible?(node)
71
+ return false unless node.end_keyword_loc # block-form only
72
+ return false if node.subsequent # no else/elsif
73
+ return false unless node.statements&.body&.any?
74
+ return false unless first_statement_in_def?(node)
75
+
76
+ true
77
+ end
78
+
79
+ def first_statement_in_def?(if_node)
80
+ return false unless @current_def
81
+
82
+ body = @current_def.body
83
+ return false unless body.is_a?(Prism::StatementsNode)
84
+
85
+ body.body.first.equal?(if_node)
86
+ end
87
+
88
+ def emit_early_return(node)
89
+ indent = indent_for(node)
90
+ cond_src = node.predicate.location.slice.strip
91
+ body_lines = node.statements.body
92
+ .map { |s| "#{indent}#{s.location.slice.strip}" }
93
+ .join("\n")
94
+
95
+ new_text = "#{indent}return unless #{cond_src}\n#{body_lines}"
96
+
97
+ @response_builder << Interface::CodeAction.new(
98
+ title: "Convert to early return",
99
+ kind: Constant::CodeActionKind::REFACTOR_REWRITE,
100
+ edit: single_edit_workspace_edit(node, new_text)
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "../support/node_helpers"
5
+
6
+ module RubyLsp
7
+ module Refactor
8
+ # Offers "Extract to include file" when the cursor is on a ModuleNode or
9
+ # ClassNode that coexists with other top-level statements in the same file.
10
+ #
11
+ # The action:
12
+ # 1. Creates a new file named after the module/class (snake_case) in the
13
+ # same directory as the source file.
14
+ # 2. Writes the extracted node's source into the new file, prefixed with
15
+ # the standard `# frozen_string_literal: true` magic comment.
16
+ # 3. Replaces the extracted node in the source file with a
17
+ # `require_relative` statement.
18
+ #
19
+ # This uses `document_changes` (not `changes`) in the WorkspaceEdit so
20
+ # that the LSP client can handle the CreateFile resource operation.
21
+ #
22
+ # Input (cursor on the module, file also contains User class):
23
+ # # app/models/user.rb
24
+ # module Greetable
25
+ # def greet = "hello"
26
+ # end
27
+ #
28
+ # class User
29
+ # include Greetable
30
+ # end
31
+ #
32
+ # Output:
33
+ # # app/models/greetable.rb (new file)
34
+ # # frozen_string_literal: true
35
+ #
36
+ # module Greetable
37
+ # def greet = "hello"
38
+ # end
39
+ #
40
+ # # app/models/user.rb (modified)
41
+ # require_relative "greetable"
42
+ #
43
+ # class User
44
+ # include Greetable
45
+ # end
46
+ class ExtractIncludeFileListener
47
+ include RubyLsp::Requests::Support::Common
48
+ include Support::NodeHelpers
49
+
50
+ def initialize(response_builder, node_context, dispatcher)
51
+ @response_builder = response_builder
52
+ @node_context = node_context
53
+ @top_level_count = 0
54
+
55
+ dispatcher.register(
56
+ self,
57
+ :on_program_node_enter,
58
+ :on_module_node_enter,
59
+ :on_class_node_enter
60
+ )
61
+ end
62
+
63
+ # Count top-level statements so we know whether extraction is meaningful.
64
+ def on_program_node_enter(node)
65
+ @top_level_count = node.statements.body.length
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
70
+ def on_module_node_enter(node)
71
+ return unless node_covers_cursor?(node)
72
+ return unless top_level_node?(node)
73
+ return unless @top_level_count > 1
74
+
75
+ emit_extract(node, module_name(node))
76
+ rescue StandardError
77
+ nil
78
+ end
79
+
80
+ def on_class_node_enter(node)
81
+ return unless node_covers_cursor?(node)
82
+ return unless top_level_node?(node)
83
+ return unless @top_level_count > 1
84
+
85
+ emit_extract(node, class_name(node))
86
+ rescue StandardError
87
+ nil
88
+ end
89
+
90
+ private
91
+
92
+ # A node is top-level when its start column is 0 (not nested inside
93
+ # another class/module).
94
+ def top_level_node?(node)
95
+ node.location.start_column.zero?
96
+ end
97
+
98
+ def module_name(node)
99
+ node.constant_path.location.slice
100
+ end
101
+
102
+ def class_name(node)
103
+ node.constant_path.location.slice
104
+ end
105
+
106
+ def emit_extract(node, const_name)
107
+ source_uri_str = @node_context.uri
108
+ new_uri_str = new_file_uri(source_uri_str, const_name)
109
+ require_name = snake_case(const_name)
110
+ node_src = node.location.slice
111
+
112
+ # ── 1. Create the new file ──────────────────────────────────────────
113
+ create_op = create_file_operation(new_uri_str)
114
+
115
+ # ── 2. Write the extracted source into the new file ─────────────────
116
+ new_file_content = "# frozen_string_literal: true\n\n#{node_src}\n"
117
+ write_new_file = text_document_edit(
118
+ new_uri_str,
119
+ [
120
+ Interface::TextEdit.new(
121
+ range: Interface::Range.new(
122
+ start: Interface::Position.new(line: 0, character: 0),
123
+ end: Interface::Position.new(line: 0, character: 0)
124
+ ),
125
+ new_text: new_file_content
126
+ )
127
+ ]
128
+ )
129
+
130
+ # ── 3. Replace the node in the source file with require_relative ────
131
+ # Include a trailing newline so the surrounding code stays clean.
132
+ require_text = "require_relative \"#{require_name}\"\n"
133
+ replace_in_src = text_document_edit(
134
+ source_uri_str,
135
+ [
136
+ Interface::TextEdit.new(
137
+ range: node_to_lsp_range(node),
138
+ new_text: require_text
139
+ )
140
+ ]
141
+ )
142
+
143
+ @response_builder << Interface::CodeAction.new(
144
+ title: "Extract to include file \"#{require_name}.rb\"",
145
+ kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
146
+ edit: multi_file_workspace_edit([create_op, write_new_file, replace_in_src])
147
+ )
148
+ end
149
+
150
+ # Derives the URI for the new file from the source file's URI and the
151
+ # constant name. The new file is placed in the same directory.
152
+ def new_file_uri(source_uri_str, const_name)
153
+ filename = "#{snake_case(const_name)}.rb"
154
+ source_uri = URI(source_uri_str)
155
+ source_dir = File.dirname(source_uri.path)
156
+ new_path = File.join(source_dir, filename)
157
+
158
+ new_uri = source_uri.dup
159
+ new_uri.path = new_path
160
+ new_uri.to_s
161
+ end
162
+
163
+ # Converts CamelCase to snake_case.
164
+ def snake_case(name)
165
+ name
166
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
167
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
168
+ .downcase
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/node_helpers"
4
+
5
+ module RubyLsp
6
+ module Refactor
7
+ # Offers "Extract predicate methods" when the cursor is on a compound
8
+ # `&&` or `||` expression that is the sole statement in a method body.
9
+ #
10
+ # From "Refactor Compound Conditionals into Methods" in Refactoring Rails:
11
+ # each operand becomes a private predicate method, making the condition
12
+ # self-documenting and each predicate independently testable.
13
+ #
14
+ # Input (cursor on the compound expression):
15
+ # def eligible_for_return?
16
+ # expired_orders.exclude?(self) && self.value > MINIMUM_RETURN_VALUE
17
+ # end
18
+ #
19
+ # Output:
20
+ # def eligible_for_return?
21
+ # predicate_1? && predicate_2?
22
+ # end
23
+ #
24
+ # private
25
+ #
26
+ # def predicate_1?
27
+ # expired_orders.exclude?(self)
28
+ # end
29
+ #
30
+ # def predicate_2?
31
+ # self.value > MINIMUM_RETURN_VALUE
32
+ # end
33
+ #
34
+ # The generated names `predicate_1?` / `predicate_2?` are intentional
35
+ # placeholders — the developer renames them to reflect intent.
36
+ class ExtractPredicateListener
37
+ include RubyLsp::Requests::Support::Common
38
+ include Support::NodeHelpers
39
+
40
+ def initialize(response_builder, node_context, dispatcher)
41
+ @response_builder = response_builder
42
+ @node_context = node_context
43
+ @current_def = nil
44
+
45
+ dispatcher.register(
46
+ self,
47
+ :on_def_node_enter,
48
+ :on_def_node_leave,
49
+ :on_and_node_enter,
50
+ :on_or_node_enter
51
+ )
52
+ end
53
+
54
+ def on_def_node_enter(node)
55
+ @current_def = node
56
+ rescue StandardError
57
+ nil
58
+ end
59
+
60
+ def on_def_node_leave(_node)
61
+ @current_def = nil
62
+ rescue StandardError
63
+ nil
64
+ end
65
+
66
+ def on_and_node_enter(node)
67
+ return unless node_covers_cursor?(node)
68
+ return unless sole_statement_in_def?(node)
69
+
70
+ emit_extract_predicates(node, "&&")
71
+ rescue StandardError
72
+ nil
73
+ end
74
+
75
+ def on_or_node_enter(node)
76
+ return unless node_covers_cursor?(node)
77
+ return unless sole_statement_in_def?(node)
78
+
79
+ emit_extract_predicates(node, "||")
80
+ rescue StandardError
81
+ nil
82
+ end
83
+
84
+ private
85
+
86
+ # The compound expression must be the only statement in the method body
87
+ # so the replacement is unambiguous.
88
+ def sole_statement_in_def?(node)
89
+ return false unless @current_def
90
+
91
+ body = @current_def.body
92
+ return false unless body.is_a?(Prism::StatementsNode)
93
+ return false unless body.body.length == 1
94
+
95
+ body.body.first.equal?(node)
96
+ end
97
+
98
+ def emit_extract_predicates(node, operator)
99
+ def_node = @current_def
100
+ indent = indent_for(def_node)
101
+ body_indent = "#{indent} "
102
+
103
+ left_src = node.left.location.slice.strip
104
+ right_src = node.right.location.slice.strip
105
+
106
+ # Replace the compound expression with calls to the two new predicates.
107
+ replace_edit = Interface::TextEdit.new(
108
+ range: node_to_lsp_range(node),
109
+ new_text: "#{body_indent}predicate_1? #{operator} predicate_2?"
110
+ )
111
+
112
+ # Insert the two private predicate methods after the enclosing def.
113
+ insert_line = def_node.location.end_line
114
+ new_methods = "\n#{indent}private\n\n" \
115
+ "#{indent}def predicate_1?\n" \
116
+ "#{body_indent}#{left_src}\n" \
117
+ "#{indent}end\n\n" \
118
+ "#{indent}def predicate_2?\n" \
119
+ "#{body_indent}#{right_src}\n" \
120
+ "#{indent}end\n"
121
+
122
+ insert_edit = Interface::TextEdit.new(
123
+ range: Interface::Range.new(
124
+ start: Interface::Position.new(line: insert_line, character: 0),
125
+ end: Interface::Position.new(line: insert_line, character: 0)
126
+ ),
127
+ new_text: new_methods
128
+ )
129
+
130
+ @response_builder << Interface::CodeAction.new(
131
+ title: "Extract predicate methods",
132
+ kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
133
+ edit: multi_edit_workspace_edit([replace_edit, insert_edit])
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
@@ -8,27 +8,22 @@ module RubyLsp
8
8
  #
9
9
  # Emitted actions
10
10
  # ───────────────
11
- # 1. Extract to method
12
- # Cursor on a LocalVariableWriteNode inside a def body.
13
- # Extracts the assignment RHS (and the assignment itself) into a new
14
- # private method, passing any variables that are defined before the
15
- # extraction point and referenced inside the extracted expression as
16
- # parameters.
17
- #
18
- # 2. Add parameter
11
+ # 1. Add parameter
19
12
  # Cursor anywhere inside a DefNode.
20
13
  # Appends a `new_param` placeholder at the end of the parameter list
21
14
  # (or creates parentheses if the method has none).
22
15
  #
23
- # 3. Convert to keyword arguments
16
+ # 2. Convert to keyword arguments
24
17
  # Cursor anywhere inside a DefNode that has required positional params.
25
- # Rewrites `def foo(a, b)` → `def foo(a:, b:)` and updates every
26
- # call-site within the same file that passes positional arguments.
18
+ # Rewrites `def foo(a, b)` → `def foo(a:, b:)`.
27
19
  #
28
- # 4. Extract to let (RSpec)
20
+ # 3. Extract to let (RSpec)
29
21
  # Cursor on a LocalVariableWriteNode inside an RSpec `it`/`specify`
30
22
  # block. Moves the assignment into a `let(:name) { value }` block
31
23
  # inserted above the enclosing example group call.
24
+ #
25
+ # Note: "Extract to method" is provided by ruby-lsp upstream as
26
+ # "Refactor: Extract Method" and is intentionally not duplicated here.
32
27
  class MethodListener
33
28
  include RubyLsp::Requests::Support::Common
34
29
  include Support::NodeHelpers
@@ -44,9 +39,6 @@ module RubyLsp
44
39
  # Each entry is a Hash with :type and the node itself.
45
40
  @ancestor_stack = []
46
41
 
47
- # All write nodes seen so far (for "defined before extraction point").
48
- @seen_writes = []
49
-
50
42
  dispatcher.register(
51
43
  self,
52
44
  :on_def_node_enter,
@@ -74,7 +66,6 @@ module RubyLsp
74
66
 
75
67
  def on_def_node_leave(_node)
76
68
  @ancestor_stack.pop
77
- @seen_writes.clear
78
69
  rescue StandardError
79
70
  nil
80
71
  end
@@ -92,19 +83,11 @@ module RubyLsp
92
83
  end
93
84
 
94
85
  def on_local_variable_write_node_enter(node)
95
- enclosing_def = nearest_ancestor(:def)
96
86
  enclosing_call = nearest_ancestor(:call)
97
87
 
98
- # Always track writes for param-detection, regardless of cursor position.
99
- @seen_writes << node
100
-
101
88
  return unless node_covers_cursor?(node)
102
89
 
103
- if enclosing_call && rspec_example?(enclosing_call[:node])
104
- emit_extract_to_let(node, enclosing_call[:node])
105
- elsif enclosing_def
106
- emit_extract_to_method(node, enclosing_def[:node])
107
- end
90
+ emit_extract_to_let(node, enclosing_call[:node]) if enclosing_call && rspec_example?(enclosing_call[:node])
108
91
  rescue StandardError
109
92
  nil
110
93
  end
@@ -117,62 +100,7 @@ module RubyLsp
117
100
  @ancestor_stack.reverse.find { |a| a[:type] == type }
118
101
  end
119
102
 
120
- # ── 1. Extract to method ─────────────────────────────────────────────────
121
-
122
- def emit_extract_to_method(write_node, def_node)
123
- method_name = write_node.name.to_s
124
- rhs_src = write_node.value.location.slice.strip
125
- indent = indent_for(def_node)
126
- body_indent = "#{indent} "
127
-
128
- # Determine which variables defined before this write are referenced
129
- # inside the RHS expression.
130
- params = params_needed_for(write_node.value, def_node)
131
- param_list = params.empty? ? "" : "(#{params.join(", ")})"
132
- call_args = params.empty? ? "" : "(#{params.join(", ")})"
133
-
134
- # Replace the assignment RHS with a call to the new method.
135
- replace_edit = Interface::TextEdit.new(
136
- range: node_to_lsp_range(write_node.value),
137
- new_text: "#{method_name}#{call_args}"
138
- )
139
-
140
- # Insert the new private method after the enclosing def's closing `end`.
141
- insert_line = def_node.location.end_line # 1-based; insert after this line
142
- new_method = "\n#{body_indent}private\n\n" \
143
- "#{body_indent}def #{method_name}#{param_list}\n" \
144
- "#{body_indent} #{rhs_src}\n" \
145
- "#{body_indent}end\n"
146
-
147
- insert_edit = Interface::TextEdit.new(
148
- range: Interface::Range.new(
149
- start: Interface::Position.new(line: insert_line, character: 0),
150
- end: Interface::Position.new(line: insert_line, character: 0)
151
- ),
152
- new_text: new_method
153
- )
154
-
155
- @response_builder << Interface::CodeAction.new(
156
- title: "Extract to method '#{method_name}'",
157
- kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
158
- edit: multi_edit_workspace_edit([replace_edit, insert_edit])
159
- )
160
- end
161
-
162
- # Collect names of variables that are:
163
- # a) written before the extraction point in the same def body, AND
164
- # b) referenced (read) inside +expr_node+.
165
- def params_needed_for(expr_node, _def_node)
166
- expr_src = expr_node.location.slice
167
-
168
- @seen_writes
169
- .select { |w| w.location.start_offset < expr_node.location.start_offset }
170
- .map { |w| w.name.to_s }
171
- .select { |name| expr_src.match?(/\b#{Regexp.escape(name)}\b/) }
172
- .uniq
173
- end
174
-
175
- # ── 2. Add parameter ─────────────────────────────────────────────────────
103
+ # ── 1. Add parameter ─────────────────────────────────────────────────────
176
104
 
177
105
  def emit_add_parameter(def_node)
178
106
  if def_node.parameters
@@ -220,7 +148,7 @@ module RubyLsp
220
148
  ].flatten.compact.last
221
149
  end
222
150
 
223
- # ── 3. Convert to keyword arguments ──────────────────────────────────────
151
+ # ── 2. Convert to keyword arguments ──────────────────────────────────────
224
152
 
225
153
  def has_positional_params?(def_node)
226
154
  def_node.parameters&.requireds&.any? { |p| p.is_a?(Prism::RequiredParameterNode) }
@@ -277,7 +205,7 @@ module RubyLsp
277
205
  parts
278
206
  end
279
207
 
280
- # ── 4. Extract to let (RSpec) ─────────────────────────────────────────────
208
+ # ── 3. Extract to let (RSpec) ─────────────────────────────────────────────
281
209
 
282
210
  RSPEC_EXAMPLE_METHODS = %i[it specify example scenario].freeze
283
211