ruby-lsp-refactor 0.1.0 → 0.1.1
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/lib/ruby/lsp/refactor/version.rb +1 -1
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +35 -9
- 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/block_style_listener.rb +117 -0
- 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/enumerable_listener.rb +90 -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 +34 -34
- 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/variable_listener.rb +11 -11
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +12 -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/block_style_listener_test.rb +98 -0
- data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
- data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
- data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
- 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
- metadata +36 -13
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Detects common Enumerable method-chain patterns and offers a single
|
|
8
|
+
# collapsed replacement.
|
|
9
|
+
#
|
|
10
|
+
# Emitted actions
|
|
11
|
+
# ───────────────
|
|
12
|
+
# 1. map + flatten(1) / flatten → flat_map
|
|
13
|
+
# items.map { |i| i.tags }.flatten(1) → items.flat_map { |i| i.tags }
|
|
14
|
+
#
|
|
15
|
+
# 2. select + first → find
|
|
16
|
+
# users.select { |u| u.admin? }.first → users.find { |u| u.admin? }
|
|
17
|
+
#
|
|
18
|
+
# 3. map + compact → filter_map
|
|
19
|
+
# items.map { |i| i.value }.compact → items.filter_map { |i| i.value }
|
|
20
|
+
#
|
|
21
|
+
# All three patterns share the same structure:
|
|
22
|
+
# outer_call( receiver: inner_call( block: BlockNode ) )
|
|
23
|
+
# where outer_call has no block of its own.
|
|
24
|
+
class EnumerableListener
|
|
25
|
+
include RubyLsp::Requests::Support::Common
|
|
26
|
+
include Support::NodeHelpers
|
|
27
|
+
|
|
28
|
+
# { outer_method => { inner_method => replacement } }
|
|
29
|
+
PATTERNS = {
|
|
30
|
+
flatten: { map: "flat_map" },
|
|
31
|
+
first: { select: "find" },
|
|
32
|
+
compact: { map: "filter_map" }
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
36
|
+
@response_builder = response_builder
|
|
37
|
+
@node_context = node_context
|
|
38
|
+
|
|
39
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_call_node_enter(node)
|
|
43
|
+
return unless node_covers_cursor?(node)
|
|
44
|
+
return if node.block # outer call must not have its own block
|
|
45
|
+
|
|
46
|
+
outer_name = node.name
|
|
47
|
+
return unless PATTERNS.key?(outer_name)
|
|
48
|
+
|
|
49
|
+
# flatten is only eligible when called with no args or with arg `1`.
|
|
50
|
+
return if outer_name == :flatten && !flatten_eligible?(node)
|
|
51
|
+
|
|
52
|
+
inner = node.receiver
|
|
53
|
+
return unless inner.is_a?(Prism::CallNode) && inner.block.is_a?(Prism::BlockNode)
|
|
54
|
+
|
|
55
|
+
replacement = PATTERNS[outer_name][inner.name]
|
|
56
|
+
return unless replacement
|
|
57
|
+
|
|
58
|
+
emit_collapse(node, inner, replacement)
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# flatten() and flatten(1) are eligible; flatten(2+) changes semantics.
|
|
66
|
+
def flatten_eligible?(node)
|
|
67
|
+
args = node.arguments&.arguments
|
|
68
|
+
return true if args.nil? || args.empty?
|
|
69
|
+
return true if args.length == 1 && args.first.is_a?(Prism::IntegerNode) &&
|
|
70
|
+
args.first.location.slice == "1"
|
|
71
|
+
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit_collapse(outer_node, inner_node, replacement)
|
|
76
|
+
# Reconstruct: <receiver>.<replacement> <block>
|
|
77
|
+
receiver_src = inner_node.receiver.location.slice.strip
|
|
78
|
+
block_src = inner_node.block.location.slice.strip
|
|
79
|
+
|
|
80
|
+
new_text = "#{indent_for(outer_node)}#{receiver_src}.#{replacement} #{block_src}"
|
|
81
|
+
|
|
82
|
+
@response_builder << Interface::CodeAction.new(
|
|
83
|
+
title: "Convert to .#{replacement}",
|
|
84
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
85
|
+
edit: single_edit_workspace_edit(outer_node, new_text)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -67,16 +67,16 @@ module RubyLsp
|
|
|
67
67
|
|
|
68
68
|
# Reconstruct the hash preserving the outer braces when present.
|
|
69
69
|
# HashNode has opening/closing braces; KeywordHashNode does not.
|
|
70
|
-
if node.is_a?(Prism::HashNode)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
new_text = if node.is_a?(Prism::HashNode)
|
|
71
|
+
"{ #{new_pairs.join(", ")} }"
|
|
72
|
+
else
|
|
73
|
+
new_pairs.join(", ")
|
|
74
|
+
end
|
|
75
75
|
|
|
76
76
|
@response_builder << Interface::CodeAction.new(
|
|
77
77
|
title: "Convert to keyword syntax",
|
|
78
|
-
kind:
|
|
79
|
-
edit:
|
|
78
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
79
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Toggles between symbolic and word forms of logical operators.
|
|
8
|
+
#
|
|
9
|
+
# Emitted actions
|
|
10
|
+
# ───────────────
|
|
11
|
+
# AndNode:
|
|
12
|
+
# user.valid? && user.save → user.valid? and user.save
|
|
13
|
+
# user.valid? and user.save → user.valid? && user.save
|
|
14
|
+
#
|
|
15
|
+
# OrNode:
|
|
16
|
+
# a || b → a or b
|
|
17
|
+
# a or b → a || b
|
|
18
|
+
class LogicalOperatorListener
|
|
19
|
+
include RubyLsp::Requests::Support::Common
|
|
20
|
+
include Support::NodeHelpers
|
|
21
|
+
|
|
22
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
23
|
+
@response_builder = response_builder
|
|
24
|
+
@node_context = node_context
|
|
25
|
+
|
|
26
|
+
dispatcher.register(self, :on_and_node_enter, :on_or_node_enter)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_and_node_enter(node)
|
|
30
|
+
return unless node_covers_cursor?(node)
|
|
31
|
+
|
|
32
|
+
if node.operator_loc.slice == "&&"
|
|
33
|
+
emit_toggle(node, "&&", "and")
|
|
34
|
+
else
|
|
35
|
+
emit_toggle(node, "and", "&&")
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def on_or_node_enter(node)
|
|
42
|
+
return unless node_covers_cursor?(node)
|
|
43
|
+
|
|
44
|
+
if node.operator_loc.slice == "||"
|
|
45
|
+
emit_toggle(node, "||", "or")
|
|
46
|
+
else
|
|
47
|
+
emit_toggle(node, "or", "||")
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def emit_toggle(node, from_op, to_op)
|
|
56
|
+
left_src = node.left.location.slice.strip
|
|
57
|
+
right_src = node.right.location.slice.strip
|
|
58
|
+
new_text = "#{indent_for(node)}#{left_src} #{to_op} #{right_src}"
|
|
59
|
+
|
|
60
|
+
title = "Convert '#{from_op}' to '#{to_op}'"
|
|
61
|
+
|
|
62
|
+
@response_builder << Interface::CodeAction.new(
|
|
63
|
+
title: title,
|
|
64
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
65
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -53,7 +53,7 @@ module RubyLsp
|
|
|
53
53
|
:on_def_node_leave,
|
|
54
54
|
:on_call_node_enter,
|
|
55
55
|
:on_call_node_leave,
|
|
56
|
-
:on_local_variable_write_node_enter
|
|
56
|
+
:on_local_variable_write_node_enter
|
|
57
57
|
)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -133,12 +133,12 @@ module RubyLsp
|
|
|
133
133
|
|
|
134
134
|
# Replace the assignment RHS with a call to the new method.
|
|
135
135
|
replace_edit = Interface::TextEdit.new(
|
|
136
|
-
range:
|
|
137
|
-
new_text: "#{method_name}#{call_args}"
|
|
136
|
+
range: node_to_lsp_range(write_node.value),
|
|
137
|
+
new_text: "#{method_name}#{call_args}"
|
|
138
138
|
)
|
|
139
139
|
|
|
140
140
|
# Insert the new private method after the enclosing def's closing `end`.
|
|
141
|
-
insert_line = def_node.location.end_line
|
|
141
|
+
insert_line = def_node.location.end_line # 1-based; insert after this line
|
|
142
142
|
new_method = "\n#{body_indent}private\n\n" \
|
|
143
143
|
"#{body_indent}def #{method_name}#{param_list}\n" \
|
|
144
144
|
"#{body_indent} #{rhs_src}\n" \
|
|
@@ -147,22 +147,22 @@ module RubyLsp
|
|
|
147
147
|
insert_edit = Interface::TextEdit.new(
|
|
148
148
|
range: Interface::Range.new(
|
|
149
149
|
start: Interface::Position.new(line: insert_line, character: 0),
|
|
150
|
-
end:
|
|
150
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
151
151
|
),
|
|
152
|
-
new_text: new_method
|
|
152
|
+
new_text: new_method
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
@response_builder << Interface::CodeAction.new(
|
|
156
156
|
title: "Extract to method '#{method_name}'",
|
|
157
|
-
kind:
|
|
158
|
-
edit:
|
|
157
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
158
|
+
edit: multi_edit_workspace_edit([replace_edit, insert_edit])
|
|
159
159
|
)
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Collect names of variables that are:
|
|
163
163
|
# a) written before the extraction point in the same def body, AND
|
|
164
164
|
# b) referenced (read) inside +expr_node+.
|
|
165
|
-
def params_needed_for(expr_node,
|
|
165
|
+
def params_needed_for(expr_node, _def_node)
|
|
166
166
|
expr_src = expr_node.location.slice
|
|
167
167
|
|
|
168
168
|
@seen_writes
|
|
@@ -185,9 +185,9 @@ module RubyLsp
|
|
|
185
185
|
edit = Interface::TextEdit.new(
|
|
186
186
|
range: Interface::Range.new(
|
|
187
187
|
start: Interface::Position.new(line: insert_line, character: insert_col),
|
|
188
|
-
end:
|
|
188
|
+
end: Interface::Position.new(line: insert_line, character: insert_col)
|
|
189
189
|
),
|
|
190
|
-
new_text: new_text_fragment
|
|
190
|
+
new_text: new_text_fragment
|
|
191
191
|
)
|
|
192
192
|
else
|
|
193
193
|
# No parameters yet — insert `(new_param)` right after the method name.
|
|
@@ -197,16 +197,16 @@ module RubyLsp
|
|
|
197
197
|
edit = Interface::TextEdit.new(
|
|
198
198
|
range: Interface::Range.new(
|
|
199
199
|
start: Interface::Position.new(line: name_end_line, character: name_end_col),
|
|
200
|
-
end:
|
|
200
|
+
end: Interface::Position.new(line: name_end_line, character: name_end_col)
|
|
201
201
|
),
|
|
202
|
-
new_text: "(new_param)"
|
|
202
|
+
new_text: "(new_param)"
|
|
203
203
|
)
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
@response_builder << Interface::CodeAction.new(
|
|
207
207
|
title: "Add parameter",
|
|
208
|
-
kind:
|
|
209
|
-
edit:
|
|
208
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
209
|
+
edit: multi_edit_workspace_edit([edit])
|
|
210
210
|
)
|
|
211
211
|
end
|
|
212
212
|
|
|
@@ -216,7 +216,7 @@ module RubyLsp
|
|
|
216
216
|
params_node.requireds,
|
|
217
217
|
params_node.optionals,
|
|
218
218
|
params_node.keywords,
|
|
219
|
-
[params_node.rest, params_node.keyword_rest, params_node.block].compact
|
|
219
|
+
[params_node.rest, params_node.keyword_rest, params_node.block].compact
|
|
220
220
|
].flatten.compact.last
|
|
221
221
|
end
|
|
222
222
|
|
|
@@ -238,21 +238,21 @@ module RubyLsp
|
|
|
238
238
|
# Replace the entire parameters span (between the parens).
|
|
239
239
|
params_range = Interface::Range.new(
|
|
240
240
|
start: Interface::Position.new(
|
|
241
|
-
line:
|
|
242
|
-
character: params_node.location.start_column
|
|
241
|
+
line: params_node.location.start_line - 1,
|
|
242
|
+
character: params_node.location.start_column
|
|
243
243
|
),
|
|
244
244
|
end: Interface::Position.new(
|
|
245
|
-
line:
|
|
246
|
-
character: params_node.location.end_column
|
|
247
|
-
)
|
|
245
|
+
line: params_node.location.end_line - 1,
|
|
246
|
+
character: params_node.location.end_column
|
|
247
|
+
)
|
|
248
248
|
)
|
|
249
249
|
|
|
250
250
|
edit = Interface::TextEdit.new(range: params_range, new_text: new_params)
|
|
251
251
|
|
|
252
252
|
@response_builder << Interface::CodeAction.new(
|
|
253
253
|
title: "Convert to keyword arguments",
|
|
254
|
-
kind:
|
|
255
|
-
edit:
|
|
254
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
255
|
+
edit: multi_edit_workspace_edit([edit])
|
|
256
256
|
)
|
|
257
257
|
end
|
|
258
258
|
|
|
@@ -261,16 +261,16 @@ module RubyLsp
|
|
|
261
261
|
parts = []
|
|
262
262
|
|
|
263
263
|
params_node.requireds.each do |p|
|
|
264
|
-
if required_names.include?(p.name)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
parts << if required_names.include?(p.name)
|
|
265
|
+
"#{p.name}:"
|
|
266
|
+
else
|
|
267
|
+
p.location.slice.strip
|
|
268
|
+
end
|
|
269
269
|
end
|
|
270
270
|
|
|
271
271
|
params_node.optionals.each { |p| parts << p.location.slice.strip }
|
|
272
|
-
parts << params_node.rest.location.slice.strip
|
|
273
|
-
params_node.keywords.each
|
|
272
|
+
parts << params_node.rest.location.slice.strip if params_node.rest
|
|
273
|
+
params_node.keywords.each { |p| parts << p.location.slice.strip }
|
|
274
274
|
parts << params_node.keyword_rest.location.slice.strip if params_node.keyword_rest
|
|
275
275
|
parts << params_node.block.location.slice.strip if params_node.block
|
|
276
276
|
|
|
@@ -297,9 +297,9 @@ module RubyLsp
|
|
|
297
297
|
insert_edit = Interface::TextEdit.new(
|
|
298
298
|
range: Interface::Range.new(
|
|
299
299
|
start: Interface::Position.new(line: insert_line, character: 0),
|
|
300
|
-
end:
|
|
300
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
301
301
|
),
|
|
302
|
-
new_text: let_text
|
|
302
|
+
new_text: let_text
|
|
303
303
|
)
|
|
304
304
|
|
|
305
305
|
# Delete the original assignment line inside the example.
|
|
@@ -307,8 +307,8 @@ module RubyLsp
|
|
|
307
307
|
|
|
308
308
|
@response_builder << Interface::CodeAction.new(
|
|
309
309
|
title: "Extract to let(:#{var_name})",
|
|
310
|
-
kind:
|
|
311
|
-
edit:
|
|
310
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
311
|
+
edit: multi_edit_workspace_edit([insert_edit, delete_edit])
|
|
312
312
|
)
|
|
313
313
|
end
|
|
314
314
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Simplifies redundant RuntimeError raises.
|
|
8
|
+
#
|
|
9
|
+
# Emitted actions
|
|
10
|
+
# ───────────────
|
|
11
|
+
# 1. Simplify raise
|
|
12
|
+
# raise RuntimeError, "msg" → raise "msg"
|
|
13
|
+
# fail RuntimeError, "msg" → fail "msg"
|
|
14
|
+
#
|
|
15
|
+
# RuntimeError is Ruby's default exception class; passing it explicitly
|
|
16
|
+
# is redundant. Only the two-argument form (class, message) is handled —
|
|
17
|
+
# `raise RuntimeError.new("msg")` is left alone because the intent may be
|
|
18
|
+
# to call a custom initializer.
|
|
19
|
+
class RaiseListener
|
|
20
|
+
include RubyLsp::Requests::Support::Common
|
|
21
|
+
include Support::NodeHelpers
|
|
22
|
+
|
|
23
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
24
|
+
@response_builder = response_builder
|
|
25
|
+
@node_context = node_context
|
|
26
|
+
|
|
27
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_call_node_enter(node)
|
|
31
|
+
return unless node_covers_cursor?(node)
|
|
32
|
+
return unless raise_or_fail?(node)
|
|
33
|
+
return unless redundant_runtime_error?(node)
|
|
34
|
+
|
|
35
|
+
emit_simplify_raise(node)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def raise_or_fail?(node)
|
|
43
|
+
%i[raise fail].include?(node.name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns true when the call is `raise RuntimeError, <message>` —
|
|
47
|
+
# exactly two arguments where the first is the constant RuntimeError.
|
|
48
|
+
def redundant_runtime_error?(node)
|
|
49
|
+
args = node.arguments&.arguments
|
|
50
|
+
return false unless args&.length == 2
|
|
51
|
+
|
|
52
|
+
first = args[0]
|
|
53
|
+
first.is_a?(Prism::ConstantReadNode) && first.name == :RuntimeError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit_simplify_raise(node)
|
|
57
|
+
keyword = node.name.to_s # "raise" or "fail"
|
|
58
|
+
msg_src = node.arguments.arguments[1].location.slice.strip
|
|
59
|
+
indent = indent_for(node)
|
|
60
|
+
new_text = "#{indent}#{keyword} #{msg_src}"
|
|
61
|
+
|
|
62
|
+
@response_builder << Interface::CodeAction.new(
|
|
63
|
+
title: "Simplify raise (remove redundant RuntimeError)",
|
|
64
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
65
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Offers "Wrap body in rescue" on any DefNode whose body does not already
|
|
8
|
+
# contain a rescue clause.
|
|
9
|
+
#
|
|
10
|
+
# Input (cursor anywhere inside the def):
|
|
11
|
+
# def call
|
|
12
|
+
# do_thing
|
|
13
|
+
# another_thing
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Output:
|
|
17
|
+
# def call
|
|
18
|
+
# do_thing
|
|
19
|
+
# another_thing
|
|
20
|
+
# rescue StandardError => e
|
|
21
|
+
# raise
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# The generated rescue clause uses `raise` to re-raise by default so the
|
|
25
|
+
# developer can fill in the actual error handling without accidentally
|
|
26
|
+
# swallowing exceptions.
|
|
27
|
+
class RescueListener
|
|
28
|
+
include RubyLsp::Requests::Support::Common
|
|
29
|
+
include Support::NodeHelpers
|
|
30
|
+
|
|
31
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
32
|
+
@response_builder = response_builder
|
|
33
|
+
@node_context = node_context
|
|
34
|
+
|
|
35
|
+
dispatcher.register(self, :on_def_node_enter)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_def_node_enter(node)
|
|
39
|
+
return unless node_covers_cursor?(node)
|
|
40
|
+
return unless node.body.is_a?(Prism::StatementsNode) # already has rescue if BeginNode
|
|
41
|
+
return if node.body.body.empty?
|
|
42
|
+
|
|
43
|
+
emit_wrap_rescue(node)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def emit_wrap_rescue(def_node)
|
|
51
|
+
indent = indent_for(def_node)
|
|
52
|
+
body_indent = "#{indent} "
|
|
53
|
+
|
|
54
|
+
# Preserve the existing body lines verbatim.
|
|
55
|
+
body_src = def_node.body.body
|
|
56
|
+
.map { |s| "#{body_indent}#{s.location.slice.strip}" }
|
|
57
|
+
.join("\n")
|
|
58
|
+
|
|
59
|
+
# Reconstruct the full def with a rescue clause appended before `end`.
|
|
60
|
+
def_header = build_def_header(def_node)
|
|
61
|
+
new_text = <<~RUBY.chomp
|
|
62
|
+
#{indent}#{def_header}
|
|
63
|
+
#{body_src}
|
|
64
|
+
#{body_indent}rescue StandardError => e
|
|
65
|
+
#{body_indent} raise
|
|
66
|
+
#{indent}end
|
|
67
|
+
RUBY
|
|
68
|
+
|
|
69
|
+
@response_builder << Interface::CodeAction.new(
|
|
70
|
+
title: "Wrap body in rescue",
|
|
71
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
72
|
+
edit: single_edit_workspace_edit(def_node, new_text)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reconstructs the `def name(params)` header line from the node's locations.
|
|
77
|
+
def build_def_header(node)
|
|
78
|
+
src = node.location.slice
|
|
79
|
+
# Take everything up to and including the closing paren (or method name
|
|
80
|
+
# when there are no params), stopping before the newline.
|
|
81
|
+
src.lines.first.rstrip
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Toggles between `let` and `let!` on RSpec lazy/eager memoization helpers.
|
|
8
|
+
#
|
|
9
|
+
# Emitted actions
|
|
10
|
+
# ───────────────
|
|
11
|
+
# 1. Convert let → let!
|
|
12
|
+
# let(:user) { User.new } → let!(:user) { User.new }
|
|
13
|
+
#
|
|
14
|
+
# 2. Convert let! → let
|
|
15
|
+
# let!(:user) { User.new } → let(:user) { User.new }
|
|
16
|
+
class RspecLetListener
|
|
17
|
+
include RubyLsp::Requests::Support::Common
|
|
18
|
+
include Support::NodeHelpers
|
|
19
|
+
|
|
20
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
21
|
+
@response_builder = response_builder
|
|
22
|
+
@node_context = node_context
|
|
23
|
+
|
|
24
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_call_node_enter(node)
|
|
28
|
+
return unless node_covers_cursor?(node)
|
|
29
|
+
return unless let_call?(node)
|
|
30
|
+
|
|
31
|
+
if node.name == :let
|
|
32
|
+
emit_toggle(node, "let", "let!")
|
|
33
|
+
else
|
|
34
|
+
emit_toggle(node, "let!", "let")
|
|
35
|
+
end
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def let_call?(node)
|
|
43
|
+
%i[let let!].include?(node.name) &&
|
|
44
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
45
|
+
node.arguments&.arguments&.first.is_a?(Prism::SymbolNode)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit_toggle(node, from_kw, to_kw)
|
|
49
|
+
# Replace only the method name portion, preserving arguments and block.
|
|
50
|
+
src = node.location.slice
|
|
51
|
+
new_text = "#{indent_for(node)}#{src.sub(/\A(\s*)#{Regexp.escape(from_kw)}/, "\\1#{to_kw}")}"
|
|
52
|
+
|
|
53
|
+
@response_builder << Interface::CodeAction.new(
|
|
54
|
+
title: "Convert #{from_kw} to #{to_kw}",
|
|
55
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
56
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Converts a bracket array of plain string literals into a %w[] word array,
|
|
8
|
+
# and vice-versa.
|
|
9
|
+
#
|
|
10
|
+
# Emitted actions
|
|
11
|
+
# ───────────────
|
|
12
|
+
# 1. Convert to string array (%w[])
|
|
13
|
+
# ["foo", "bar", "baz"] → %w[foo bar baz]
|
|
14
|
+
#
|
|
15
|
+
# 2. Convert to bracket array
|
|
16
|
+
# %w[foo bar baz] → ["foo", "bar", "baz"]
|
|
17
|
+
#
|
|
18
|
+
# Only plain string literals with no interpolation and no spaces in their
|
|
19
|
+
# content are eligible for compression into %w[].
|
|
20
|
+
class StringArrayListener
|
|
21
|
+
include RubyLsp::Requests::Support::Common
|
|
22
|
+
include Support::NodeHelpers
|
|
23
|
+
|
|
24
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
25
|
+
@response_builder = response_builder
|
|
26
|
+
@node_context = node_context
|
|
27
|
+
|
|
28
|
+
dispatcher.register(self, :on_array_node_enter)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_array_node_enter(node)
|
|
32
|
+
return unless node_covers_cursor?(node)
|
|
33
|
+
return if node.elements.empty?
|
|
34
|
+
|
|
35
|
+
if bracket_string_array?(node)
|
|
36
|
+
emit_to_percent_w(node)
|
|
37
|
+
elsif percent_w_array?(node)
|
|
38
|
+
emit_to_bracket(node)
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Returns true when every element is a plain double-quoted StringNode
|
|
47
|
+
# with no interpolation and no spaces in its content.
|
|
48
|
+
def bracket_string_array?(node)
|
|
49
|
+
return false unless node.opening_loc&.slice == "["
|
|
50
|
+
|
|
51
|
+
node.elements.all? do |el|
|
|
52
|
+
el.is_a?(Prism::StringNode) &&
|
|
53
|
+
el.opening_loc&.slice == '"' &&
|
|
54
|
+
!el.unescaped.include?(" ") &&
|
|
55
|
+
!el.unescaped.include?("\t")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns true when the array uses %w[] or %W[] syntax.
|
|
60
|
+
def percent_w_array?(node)
|
|
61
|
+
opening = node.opening_loc&.slice.to_s
|
|
62
|
+
opening.start_with?("%w", "%W")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def emit_to_percent_w(node)
|
|
66
|
+
words = node.elements.map(&:unescaped)
|
|
67
|
+
new_text = "%w[#{words.join(" ")}]"
|
|
68
|
+
|
|
69
|
+
@response_builder << Interface::CodeAction.new(
|
|
70
|
+
title: "Convert to string array (%w[])",
|
|
71
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
72
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def emit_to_bracket(node)
|
|
77
|
+
words = node.elements.map { |el| "\"#{el.unescaped}\"" }
|
|
78
|
+
new_text = "[#{words.join(", ")}]"
|
|
79
|
+
|
|
80
|
+
@response_builder << Interface::CodeAction.new(
|
|
81
|
+
title: "Convert to bracket array",
|
|
82
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
83
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|