ruby-lsp-refactor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +327 -0
- data/Rakefile +16 -0
- data/lib/ruby/lsp/refactor/version.rb +9 -0
- data/lib/ruby/lsp/refactor.rb +12 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +97 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb +65 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb +250 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb +97 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +316 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb +61 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +134 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +101 -0
- data/lib/ruby_lsp/test_helper.rb +50 -0
- data/sig/ruby/lsp/refactor.rbs +8 -0
- data/test/ruby_lsp_refactor/array_listener_test.rb +82 -0
- data/test/ruby_lsp_refactor/conditional_listener_test.rb +307 -0
- data/test/ruby_lsp_refactor/hash_listener_test.rb +72 -0
- data/test/ruby_lsp_refactor/method_listener_test.rb +193 -0
- data/test/ruby_lsp_refactor/string_listener_test.rb +70 -0
- data/test/ruby_lsp_refactor/variable_listener_test.rb +97 -0
- metadata +143 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "ruby_lsp/test_helper"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Refactor
|
|
8
|
+
class ConditionalListenerTest < Minitest::Test
|
|
9
|
+
include RubyLsp::Refactor::TestHelper
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Helpers
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def find_action(actions, title)
|
|
16
|
+
actions.find { |a| a.title == title }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def single_edit(action)
|
|
20
|
+
edits = action.edit.changes.values.flatten
|
|
21
|
+
assert_equal 1, edits.size, "Expected exactly one TextEdit, got #{edits.size}"
|
|
22
|
+
edits.first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ===========================================================================
|
|
26
|
+
# 1. Convert to post-conditional (block if → post-if)
|
|
27
|
+
# ===========================================================================
|
|
28
|
+
|
|
29
|
+
def test_converts_simple_if_block_to_post_conditional
|
|
30
|
+
source = <<~RUBY
|
|
31
|
+
if user.qualified?
|
|
32
|
+
user.approve!
|
|
33
|
+
end
|
|
34
|
+
RUBY
|
|
35
|
+
|
|
36
|
+
actions = code_actions_for(source, line: 0)
|
|
37
|
+
action = find_action(actions, "Convert to post-conditional")
|
|
38
|
+
refute_nil action, "Expected a 'Convert to post-conditional' code action"
|
|
39
|
+
|
|
40
|
+
edit = single_edit(action)
|
|
41
|
+
assert_equal "user.approve! if user.qualified?", edit.new_text.strip
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_does_not_offer_post_conditional_for_if_with_else
|
|
45
|
+
source = <<~RUBY
|
|
46
|
+
if user.qualified?
|
|
47
|
+
user.approve!
|
|
48
|
+
else
|
|
49
|
+
user.reject!
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
|
|
53
|
+
actions = code_actions_for(source, line: 0)
|
|
54
|
+
assert_nil find_action(actions, "Convert to post-conditional")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_does_not_offer_post_conditional_for_if_with_elsif
|
|
58
|
+
source = <<~RUBY
|
|
59
|
+
if user.admin?
|
|
60
|
+
user.grant_admin!
|
|
61
|
+
elsif user.qualified?
|
|
62
|
+
user.approve!
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
actions = code_actions_for(source, line: 0)
|
|
67
|
+
assert_nil find_action(actions, "Convert to post-conditional")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_does_not_offer_post_conditional_for_multi_statement_body
|
|
71
|
+
source = <<~RUBY
|
|
72
|
+
if user.qualified?
|
|
73
|
+
user.approve!
|
|
74
|
+
notify(user)
|
|
75
|
+
end
|
|
76
|
+
RUBY
|
|
77
|
+
|
|
78
|
+
actions = code_actions_for(source, line: 0)
|
|
79
|
+
assert_nil find_action(actions, "Convert to post-conditional")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_post_conditional_preserves_leading_indentation
|
|
83
|
+
source = <<~RUBY
|
|
84
|
+
def process
|
|
85
|
+
if user.qualified?
|
|
86
|
+
user.approve!
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
RUBY
|
|
90
|
+
|
|
91
|
+
actions = code_actions_for(source, line: 1)
|
|
92
|
+
action = find_action(actions, "Convert to post-conditional")
|
|
93
|
+
refute_nil action
|
|
94
|
+
|
|
95
|
+
edit = single_edit(action)
|
|
96
|
+
assert_match(/\A user\.approve! if user\.qualified\?/, edit.new_text)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_post_conditional_edit_range_covers_entire_if_block
|
|
100
|
+
source = <<~RUBY
|
|
101
|
+
if user.qualified?
|
|
102
|
+
user.approve!
|
|
103
|
+
end
|
|
104
|
+
RUBY
|
|
105
|
+
|
|
106
|
+
actions = code_actions_for(source, line: 0)
|
|
107
|
+
edit = single_edit(find_action(actions, "Convert to post-conditional"))
|
|
108
|
+
|
|
109
|
+
assert_equal 0, edit.range.start.line
|
|
110
|
+
assert_equal 2, edit.range.end.line
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_converts_unless_block_to_post_conditional
|
|
114
|
+
source = <<~RUBY
|
|
115
|
+
unless user.banned?
|
|
116
|
+
user.login!
|
|
117
|
+
end
|
|
118
|
+
RUBY
|
|
119
|
+
|
|
120
|
+
actions = code_actions_for(source, line: 0)
|
|
121
|
+
action = find_action(actions, "Convert to post-conditional")
|
|
122
|
+
refute_nil action
|
|
123
|
+
|
|
124
|
+
edit = single_edit(action)
|
|
125
|
+
assert_equal "user.login! unless user.banned?", edit.new_text.strip
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ===========================================================================
|
|
129
|
+
# 2. Convert to block if (post-if → block if)
|
|
130
|
+
# ===========================================================================
|
|
131
|
+
|
|
132
|
+
def test_converts_post_conditional_to_block_if
|
|
133
|
+
source = "user.approve! if user.qualified?\n"
|
|
134
|
+
|
|
135
|
+
actions = code_actions_for(source, line: 0)
|
|
136
|
+
action = find_action(actions, "Convert to block if")
|
|
137
|
+
refute_nil action, "Expected a 'Convert to block if' code action"
|
|
138
|
+
|
|
139
|
+
edit = single_edit(action)
|
|
140
|
+
assert_match(/if user\.qualified\?/, edit.new_text)
|
|
141
|
+
assert_match(/user\.approve!/, edit.new_text)
|
|
142
|
+
assert_match(/end/, edit.new_text)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_converts_post_conditional_unless_to_block_unless
|
|
146
|
+
source = "user.login! unless user.banned?\n"
|
|
147
|
+
|
|
148
|
+
actions = code_actions_for(source, line: 0)
|
|
149
|
+
action = find_action(actions, "Convert to block unless")
|
|
150
|
+
refute_nil action
|
|
151
|
+
|
|
152
|
+
edit = single_edit(action)
|
|
153
|
+
assert_match(/unless user\.banned\?/, edit.new_text)
|
|
154
|
+
assert_match(/user\.login!/, edit.new_text)
|
|
155
|
+
assert_match(/end/, edit.new_text)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ===========================================================================
|
|
159
|
+
# 3. Toggle if ↔ unless
|
|
160
|
+
# ===========================================================================
|
|
161
|
+
|
|
162
|
+
def test_toggles_if_to_unless
|
|
163
|
+
source = <<~RUBY
|
|
164
|
+
if user.active?
|
|
165
|
+
user.greet!
|
|
166
|
+
end
|
|
167
|
+
RUBY
|
|
168
|
+
|
|
169
|
+
actions = code_actions_for(source, line: 0)
|
|
170
|
+
action = find_action(actions, "Convert to unless")
|
|
171
|
+
refute_nil action
|
|
172
|
+
|
|
173
|
+
edit = single_edit(action)
|
|
174
|
+
assert_match(/unless user\.active\?/, edit.new_text)
|
|
175
|
+
assert_match(/user\.greet!/, edit.new_text)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_toggles_unless_to_if
|
|
179
|
+
source = <<~RUBY
|
|
180
|
+
unless user.banned?
|
|
181
|
+
user.login!
|
|
182
|
+
end
|
|
183
|
+
RUBY
|
|
184
|
+
|
|
185
|
+
actions = code_actions_for(source, line: 0)
|
|
186
|
+
action = find_action(actions, "Convert to if")
|
|
187
|
+
refute_nil action
|
|
188
|
+
|
|
189
|
+
edit = single_edit(action)
|
|
190
|
+
assert_match(/if user\.banned\?/, edit.new_text)
|
|
191
|
+
assert_match(/user\.login!/, edit.new_text)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_toggle_if_with_bang_predicate_strips_negation
|
|
195
|
+
source = <<~RUBY
|
|
196
|
+
if !user.banned?
|
|
197
|
+
user.login!
|
|
198
|
+
end
|
|
199
|
+
RUBY
|
|
200
|
+
|
|
201
|
+
actions = code_actions_for(source, line: 0)
|
|
202
|
+
action = find_action(actions, "Convert to unless")
|
|
203
|
+
refute_nil action
|
|
204
|
+
|
|
205
|
+
edit = single_edit(action)
|
|
206
|
+
# The `!` should be stripped: `unless user.banned?`
|
|
207
|
+
assert_match(/unless user\.banned\?/, edit.new_text)
|
|
208
|
+
refute_match(/!user/, edit.new_text)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_does_not_offer_toggle_when_else_present
|
|
212
|
+
source = <<~RUBY
|
|
213
|
+
if user.active?
|
|
214
|
+
greet!
|
|
215
|
+
else
|
|
216
|
+
reject!
|
|
217
|
+
end
|
|
218
|
+
RUBY
|
|
219
|
+
|
|
220
|
+
# Toggle should not appear; invert should appear instead.
|
|
221
|
+
actions = code_actions_for(source, line: 0)
|
|
222
|
+
assert_nil find_action(actions, "Convert to unless")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ===========================================================================
|
|
226
|
+
# 4. Invert if/else
|
|
227
|
+
# ===========================================================================
|
|
228
|
+
|
|
229
|
+
def test_inverts_if_else_branches
|
|
230
|
+
source = <<~RUBY
|
|
231
|
+
if user.admin?
|
|
232
|
+
grant!
|
|
233
|
+
else
|
|
234
|
+
deny!
|
|
235
|
+
end
|
|
236
|
+
RUBY
|
|
237
|
+
|
|
238
|
+
actions = code_actions_for(source, line: 0)
|
|
239
|
+
action = find_action(actions, "Invert if/else")
|
|
240
|
+
refute_nil action
|
|
241
|
+
|
|
242
|
+
edit = single_edit(action)
|
|
243
|
+
# Predicate should be negated and branches swapped.
|
|
244
|
+
assert_match(/if !user\.admin\?/, edit.new_text)
|
|
245
|
+
# deny! should now be in the then-branch (first after if)
|
|
246
|
+
deny_pos = edit.new_text.index("deny!")
|
|
247
|
+
grant_pos = edit.new_text.index("grant!")
|
|
248
|
+
assert deny_pos < grant_pos, "deny! should appear before grant! after inversion"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_invert_cancels_double_negation
|
|
252
|
+
source = <<~RUBY
|
|
253
|
+
if !user.banned?
|
|
254
|
+
allow!
|
|
255
|
+
else
|
|
256
|
+
block!
|
|
257
|
+
end
|
|
258
|
+
RUBY
|
|
259
|
+
|
|
260
|
+
actions = code_actions_for(source, line: 0)
|
|
261
|
+
action = find_action(actions, "Invert if/else")
|
|
262
|
+
refute_nil action
|
|
263
|
+
|
|
264
|
+
edit = single_edit(action)
|
|
265
|
+
# `!(!user.banned?)` should simplify to `user.banned?`
|
|
266
|
+
assert_match(/if user\.banned\?/, edit.new_text)
|
|
267
|
+
refute_match(/!!/, edit.new_text)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def test_does_not_offer_invert_without_else
|
|
271
|
+
source = <<~RUBY
|
|
272
|
+
if user.active?
|
|
273
|
+
greet!
|
|
274
|
+
end
|
|
275
|
+
RUBY
|
|
276
|
+
|
|
277
|
+
actions = code_actions_for(source, line: 0)
|
|
278
|
+
assert_nil find_action(actions, "Invert if/else")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# ===========================================================================
|
|
282
|
+
# Resilience
|
|
283
|
+
# ===========================================================================
|
|
284
|
+
|
|
285
|
+
def test_does_not_raise_on_empty_source
|
|
286
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_does_not_raise_on_incomplete_if
|
|
290
|
+
source = "if user.qualified?\n"
|
|
291
|
+
assert_silent { code_actions_for(source, line: 0) }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def test_does_not_offer_action_when_cursor_is_outside_node
|
|
295
|
+
source = <<~RUBY
|
|
296
|
+
if user.qualified?
|
|
297
|
+
user.approve!
|
|
298
|
+
end
|
|
299
|
+
puts "done"
|
|
300
|
+
RUBY
|
|
301
|
+
|
|
302
|
+
actions = code_actions_for(source, line: 3)
|
|
303
|
+
assert_nil find_action(actions, "Convert to post-conditional")
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "ruby_lsp/test_helper"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Refactor
|
|
8
|
+
class HashListenerTest < Minitest::Test
|
|
9
|
+
include RubyLsp::Refactor::TestHelper
|
|
10
|
+
|
|
11
|
+
def find_action(actions, title)
|
|
12
|
+
actions.find { |a| a.title == title }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def single_edit(action)
|
|
16
|
+
edits = action.edit.changes.values.flatten
|
|
17
|
+
assert_equal 1, edits.size
|
|
18
|
+
edits.first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Core acceptance
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def test_converts_hash_rocket_to_keyword_syntax
|
|
26
|
+
source = "{ :foo => 1, :bar => 2 }\n"
|
|
27
|
+
actions = code_actions_for(source, line: 0)
|
|
28
|
+
action = find_action(actions, "Convert to keyword syntax")
|
|
29
|
+
refute_nil action
|
|
30
|
+
|
|
31
|
+
edit = single_edit(action)
|
|
32
|
+
assert_equal "{ foo: 1, bar: 2 }", edit.new_text
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_converts_mixed_hash_only_rocket_pairs
|
|
36
|
+
# String key should be left verbatim; symbol rocket key should be converted.
|
|
37
|
+
source = %({ "name" => "Alice", :age => 30 }\n)
|
|
38
|
+
actions = code_actions_for(source, line: 0)
|
|
39
|
+
action = find_action(actions, "Convert to keyword syntax")
|
|
40
|
+
refute_nil action
|
|
41
|
+
|
|
42
|
+
edit = single_edit(action)
|
|
43
|
+
assert_match(/age: 30/, edit.new_text)
|
|
44
|
+
assert_match(/"name" => "Alice"/, edit.new_text)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Negative cases
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def test_does_not_offer_action_for_already_keyword_hash
|
|
52
|
+
source = "{ foo: 1, bar: 2 }\n"
|
|
53
|
+
actions = code_actions_for(source, line: 0)
|
|
54
|
+
assert_nil find_action(actions, "Convert to keyword syntax")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_does_not_offer_action_for_empty_hash
|
|
58
|
+
source = "{}\n"
|
|
59
|
+
actions = code_actions_for(source, line: 0)
|
|
60
|
+
assert_nil find_action(actions, "Convert to keyword syntax")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Resilience
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def test_does_not_raise_on_empty_source
|
|
68
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "ruby_lsp/test_helper"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Refactor
|
|
8
|
+
class MethodListenerTest < Minitest::Test
|
|
9
|
+
include RubyLsp::Refactor::TestHelper
|
|
10
|
+
|
|
11
|
+
def find_action(actions, title_pattern)
|
|
12
|
+
actions.find { |a| a.title.match?(title_pattern) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_edits(action)
|
|
16
|
+
action.edit.changes.values.flatten
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# ===========================================================================
|
|
20
|
+
# 1. Extract to method
|
|
21
|
+
# ===========================================================================
|
|
22
|
+
|
|
23
|
+
def test_extract_to_method_replaces_rhs_and_inserts_new_method
|
|
24
|
+
source = <<~RUBY
|
|
25
|
+
def process
|
|
26
|
+
result = expensive_computation
|
|
27
|
+
end
|
|
28
|
+
RUBY
|
|
29
|
+
|
|
30
|
+
actions = code_actions_for(source, line: 1)
|
|
31
|
+
action = find_action(actions, /Extract to method/)
|
|
32
|
+
refute_nil action
|
|
33
|
+
|
|
34
|
+
edits = all_edits(action)
|
|
35
|
+
assert_equal 2, edits.size
|
|
36
|
+
|
|
37
|
+
replace_edit = edits.find { |e| e.new_text.include?("result") && !e.new_text.include?("def") }
|
|
38
|
+
insert_edit = edits.find { |e| e.new_text.include?("def result") }
|
|
39
|
+
|
|
40
|
+
refute_nil replace_edit, "Expected an edit replacing the RHS with a method call"
|
|
41
|
+
refute_nil insert_edit, "Expected an edit inserting the new method definition"
|
|
42
|
+
|
|
43
|
+
assert_match(/def result/, insert_edit.new_text)
|
|
44
|
+
assert_match(/expensive_computation/, insert_edit.new_text)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_extract_to_method_passes_outer_variables_as_params
|
|
48
|
+
source = <<~RUBY
|
|
49
|
+
def process(data)
|
|
50
|
+
threshold = 10
|
|
51
|
+
result = data.select { |x| x > threshold }
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
# Cursor on the `result =` line (line 2).
|
|
56
|
+
actions = code_actions_for(source, line: 2)
|
|
57
|
+
action = find_action(actions, /Extract to method/)
|
|
58
|
+
refute_nil action
|
|
59
|
+
|
|
60
|
+
edits = all_edits(action)
|
|
61
|
+
insert_edit = edits.find { |e| e.new_text.include?("def result") }
|
|
62
|
+
refute_nil insert_edit
|
|
63
|
+
|
|
64
|
+
# `threshold` was defined before the extraction point and is used in the RHS.
|
|
65
|
+
assert_match(/def result\(threshold\)/, insert_edit.new_text)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ===========================================================================
|
|
69
|
+
# 2. Add parameter
|
|
70
|
+
# ===========================================================================
|
|
71
|
+
|
|
72
|
+
def test_add_parameter_appends_to_existing_params
|
|
73
|
+
source = <<~RUBY
|
|
74
|
+
def greet(name)
|
|
75
|
+
puts name
|
|
76
|
+
end
|
|
77
|
+
RUBY
|
|
78
|
+
|
|
79
|
+
actions = code_actions_for(source, line: 0)
|
|
80
|
+
action = find_action(actions, "Add parameter")
|
|
81
|
+
refute_nil action
|
|
82
|
+
|
|
83
|
+
edits = all_edits(action)
|
|
84
|
+
assert_equal 1, edits.size
|
|
85
|
+
|
|
86
|
+
edit = edits.first
|
|
87
|
+
assert_equal ", new_param", edit.new_text
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_add_parameter_creates_parens_when_no_params
|
|
91
|
+
source = <<~RUBY
|
|
92
|
+
def greet
|
|
93
|
+
puts "hello"
|
|
94
|
+
end
|
|
95
|
+
RUBY
|
|
96
|
+
|
|
97
|
+
actions = code_actions_for(source, line: 0)
|
|
98
|
+
action = find_action(actions, "Add parameter")
|
|
99
|
+
refute_nil action
|
|
100
|
+
|
|
101
|
+
edits = all_edits(action)
|
|
102
|
+
assert_equal 1, edits.size
|
|
103
|
+
|
|
104
|
+
edit = edits.first
|
|
105
|
+
assert_equal "(new_param)", edit.new_text
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ===========================================================================
|
|
109
|
+
# 3. Convert to keyword arguments
|
|
110
|
+
# ===========================================================================
|
|
111
|
+
|
|
112
|
+
def test_converts_positional_params_to_kwargs
|
|
113
|
+
source = <<~RUBY
|
|
114
|
+
def create(name, age)
|
|
115
|
+
User.new(name, age)
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
|
|
119
|
+
actions = code_actions_for(source, line: 0)
|
|
120
|
+
action = find_action(actions, "Convert to keyword arguments")
|
|
121
|
+
refute_nil action
|
|
122
|
+
|
|
123
|
+
edits = all_edits(action)
|
|
124
|
+
assert_equal 1, edits.size
|
|
125
|
+
|
|
126
|
+
edit = edits.first
|
|
127
|
+
assert_equal "name:, age:", edit.new_text
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_does_not_offer_kwargs_conversion_when_no_positional_params
|
|
131
|
+
source = <<~RUBY
|
|
132
|
+
def greet
|
|
133
|
+
puts "hello"
|
|
134
|
+
end
|
|
135
|
+
RUBY
|
|
136
|
+
|
|
137
|
+
actions = code_actions_for(source, line: 0)
|
|
138
|
+
assert_nil find_action(actions, "Convert to keyword arguments")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ===========================================================================
|
|
142
|
+
# 4. Extract to let (RSpec)
|
|
143
|
+
# ===========================================================================
|
|
144
|
+
|
|
145
|
+
def test_extract_to_let_inserts_let_block_and_removes_assignment
|
|
146
|
+
source = <<~RUBY
|
|
147
|
+
describe "User" do
|
|
148
|
+
it "logs in" do
|
|
149
|
+
user = User.new(name: "Alice")
|
|
150
|
+
expect(user.name).to eq("Alice")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
RUBY
|
|
154
|
+
|
|
155
|
+
# Cursor on the `user =` line (line 2).
|
|
156
|
+
actions = code_actions_for(source, line: 2)
|
|
157
|
+
action = find_action(actions, /Extract to let/)
|
|
158
|
+
refute_nil action
|
|
159
|
+
|
|
160
|
+
edits = all_edits(action)
|
|
161
|
+
assert_equal 2, edits.size
|
|
162
|
+
|
|
163
|
+
insert_edit = edits.find { |e| e.new_text.include?("let(") }
|
|
164
|
+
delete_edit = edits.find { |e| e.new_text == "" }
|
|
165
|
+
|
|
166
|
+
refute_nil insert_edit, "Expected an edit inserting a let block"
|
|
167
|
+
refute_nil delete_edit, "Expected an edit deleting the original assignment"
|
|
168
|
+
|
|
169
|
+
assert_match(/let\(:user\)/, insert_edit.new_text)
|
|
170
|
+
assert_match(/User\.new\(name: "Alice"\)/, insert_edit.new_text)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_does_not_offer_extract_to_let_outside_rspec_example
|
|
174
|
+
source = <<~RUBY
|
|
175
|
+
def setup
|
|
176
|
+
user = User.new
|
|
177
|
+
end
|
|
178
|
+
RUBY
|
|
179
|
+
|
|
180
|
+
actions = code_actions_for(source, line: 1)
|
|
181
|
+
assert_nil find_action(actions, /Extract to let/)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Resilience
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def test_does_not_raise_on_empty_source
|
|
189
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "ruby_lsp/test_helper"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Refactor
|
|
8
|
+
class StringListenerTest < Minitest::Test
|
|
9
|
+
include RubyLsp::Refactor::TestHelper
|
|
10
|
+
|
|
11
|
+
def find_action(actions, title)
|
|
12
|
+
actions.find { |a| a.title == title }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def single_edit(action)
|
|
16
|
+
edits = action.edit.changes.values.flatten
|
|
17
|
+
assert_equal 1, edits.size
|
|
18
|
+
edits.first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Core acceptance
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def test_converts_single_quoted_string_to_double_quoted
|
|
26
|
+
source = "'hello world'\n"
|
|
27
|
+
actions = code_actions_for(source, line: 0)
|
|
28
|
+
action = find_action(actions, "Convert to interpolated string")
|
|
29
|
+
refute_nil action
|
|
30
|
+
|
|
31
|
+
edit = single_edit(action)
|
|
32
|
+
assert_equal '"hello world"', edit.new_text
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_escapes_embedded_double_quotes
|
|
36
|
+
source = %('say "hi"' \n)
|
|
37
|
+
actions = code_actions_for(source, line: 0)
|
|
38
|
+
action = find_action(actions, "Convert to interpolated string")
|
|
39
|
+
refute_nil action
|
|
40
|
+
|
|
41
|
+
edit = single_edit(action)
|
|
42
|
+
assert_equal '"say \"hi\""', edit.new_text
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Negative cases
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def test_does_not_offer_action_for_already_double_quoted_string
|
|
50
|
+
source = '"hello world"' + "\n"
|
|
51
|
+
actions = code_actions_for(source, line: 0)
|
|
52
|
+
assert_nil find_action(actions, "Convert to interpolated string")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_does_not_offer_action_for_interpolated_string
|
|
56
|
+
source = '"hello #{name}"' + "\n"
|
|
57
|
+
actions = code_actions_for(source, line: 0)
|
|
58
|
+
assert_nil find_action(actions, "Convert to interpolated string")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Resilience
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def test_does_not_raise_on_empty_source
|
|
66
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|