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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +7 -7
- data/README.md +553 -115
- data/lib/ruby/lsp/refactor/version.rb +1 -1
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +34 -23
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb +105 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb +172 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb +138 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +11 -83
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb +133 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +2 -44
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +50 -0
- data/test/ruby_lsp_refactor/early_return_listener_test.rb +156 -0
- data/test/ruby_lsp_refactor/extract_include_file_listener_test.rb +189 -0
- data/test/ruby_lsp_refactor/extract_predicate_listener_test.rb +113 -0
- data/test/ruby_lsp_refactor/method_listener_test.rb +3 -52
- data/test/ruby_lsp_refactor/tap_listener_test.rb +144 -0
- data/test/ruby_lsp_refactor/variable_listener_test.rb +0 -23
- metadata +9 -3
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/block_style_listener.rb +0 -117
- data/test/ruby_lsp_refactor/block_style_listener_test.rb +0 -98
|
@@ -0,0 +1,113 @@
|
|
|
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 ExtractPredicateListenerTest < 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 all_edits(action)
|
|
16
|
+
action.edit.changes.values.flatten
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# ── core acceptance ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
def test_extracts_and_compound_into_predicate_methods
|
|
22
|
+
source = <<~RUBY
|
|
23
|
+
def eligible_for_return?
|
|
24
|
+
expired_orders.exclude?(self) && self.value > MINIMUM_RETURN_VALUE
|
|
25
|
+
end
|
|
26
|
+
RUBY
|
|
27
|
+
|
|
28
|
+
actions = code_actions_for(source, line: 1)
|
|
29
|
+
action = find_action(actions, "Extract predicate methods")
|
|
30
|
+
refute_nil action
|
|
31
|
+
|
|
32
|
+
edits = all_edits(action)
|
|
33
|
+
assert_equal 2, edits.size
|
|
34
|
+
|
|
35
|
+
replace_edit = edits.find { |e| e.new_text.include?("predicate_1?") && e.new_text.include?("&&") }
|
|
36
|
+
insert_edit = edits.find { |e| e.new_text.include?("def predicate_1?") }
|
|
37
|
+
|
|
38
|
+
refute_nil replace_edit
|
|
39
|
+
refute_nil insert_edit
|
|
40
|
+
|
|
41
|
+
assert_match(/predicate_1\? && predicate_2\?/, replace_edit.new_text)
|
|
42
|
+
assert_match(/def predicate_1\?/, insert_edit.new_text)
|
|
43
|
+
assert_match(/expired_orders\.exclude\?\(self\)/, insert_edit.new_text)
|
|
44
|
+
assert_match(/def predicate_2\?/, insert_edit.new_text)
|
|
45
|
+
assert_match(/self\.value > MINIMUM_RETURN_VALUE/, insert_edit.new_text)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_extracts_or_compound_into_predicate_methods
|
|
49
|
+
source = <<~RUBY
|
|
50
|
+
def should_notify?
|
|
51
|
+
user.admin? || user.subscribed?
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
actions = code_actions_for(source, line: 1)
|
|
56
|
+
action = find_action(actions, "Extract predicate methods")
|
|
57
|
+
refute_nil action
|
|
58
|
+
|
|
59
|
+
edits = all_edits(action)
|
|
60
|
+
replace_edit = edits.find { |e| e.new_text.include?("||") }
|
|
61
|
+
insert_edit = edits.find { |e| e.new_text.include?("def predicate_1?") }
|
|
62
|
+
|
|
63
|
+
refute_nil replace_edit
|
|
64
|
+
refute_nil insert_edit
|
|
65
|
+
assert_match(/predicate_1\? \|\| predicate_2\?/, replace_edit.new_text)
|
|
66
|
+
assert_match(/user\.admin\?/, insert_edit.new_text)
|
|
67
|
+
assert_match(/user\.subscribed\?/, insert_edit.new_text)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_inserts_private_section_after_def
|
|
71
|
+
source = <<~RUBY
|
|
72
|
+
def eligible?
|
|
73
|
+
a? && b?
|
|
74
|
+
end
|
|
75
|
+
RUBY
|
|
76
|
+
|
|
77
|
+
actions = code_actions_for(source, line: 1)
|
|
78
|
+
action = find_action(actions, "Extract predicate methods")
|
|
79
|
+
refute_nil action
|
|
80
|
+
|
|
81
|
+
edits = all_edits(action)
|
|
82
|
+
insert_edit = edits.find { |e| e.new_text.include?("def predicate_1?") }
|
|
83
|
+
assert_match(/private/, insert_edit.new_text)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ── negative cases ─────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def test_does_not_offer_when_method_has_multiple_statements
|
|
89
|
+
source = <<~RUBY
|
|
90
|
+
def process
|
|
91
|
+
validate!
|
|
92
|
+
a? && b?
|
|
93
|
+
end
|
|
94
|
+
RUBY
|
|
95
|
+
|
|
96
|
+
actions = code_actions_for(source, line: 2)
|
|
97
|
+
assert_nil find_action(actions, "Extract predicate methods")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_does_not_offer_outside_a_method
|
|
101
|
+
source = "a? && b?\n"
|
|
102
|
+
actions = code_actions_for(source, line: 0)
|
|
103
|
+
assert_nil find_action(actions, "Extract predicate methods")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ── resilience ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def test_does_not_raise_on_empty_source
|
|
109
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -17,56 +17,7 @@ module RubyLsp
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# ===========================================================================
|
|
20
|
-
# 1.
|
|
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
|
|
20
|
+
# 1. Add parameter
|
|
70
21
|
# ===========================================================================
|
|
71
22
|
|
|
72
23
|
def test_add_parameter_appends_to_existing_params
|
|
@@ -106,7 +57,7 @@ module RubyLsp
|
|
|
106
57
|
end
|
|
107
58
|
|
|
108
59
|
# ===========================================================================
|
|
109
|
-
#
|
|
60
|
+
# 2. Convert to keyword arguments
|
|
110
61
|
# ===========================================================================
|
|
111
62
|
|
|
112
63
|
def test_converts_positional_params_to_kwargs
|
|
@@ -139,7 +90,7 @@ module RubyLsp
|
|
|
139
90
|
end
|
|
140
91
|
|
|
141
92
|
# ===========================================================================
|
|
142
|
-
#
|
|
93
|
+
# 3. Extract to let (RSpec)
|
|
143
94
|
# ===========================================================================
|
|
144
95
|
|
|
145
96
|
def test_extract_to_let_inserts_let_block_and_removes_assignment
|
|
@@ -0,0 +1,144 @@
|
|
|
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 TapListenerTest < 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
|
+
# ── core acceptance ────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def test_converts_sequence_to_tap
|
|
24
|
+
source = <<~RUBY
|
|
25
|
+
def do_something
|
|
26
|
+
obj.do_first_thing
|
|
27
|
+
obj.do_second_thing
|
|
28
|
+
obj.do_third_thing
|
|
29
|
+
obj
|
|
30
|
+
end
|
|
31
|
+
RUBY
|
|
32
|
+
|
|
33
|
+
actions = code_actions_for(source, line: 0)
|
|
34
|
+
action = find_action(actions, "Convert to tap")
|
|
35
|
+
refute_nil action
|
|
36
|
+
|
|
37
|
+
edit = single_edit(action)
|
|
38
|
+
assert_match(/obj\.tap do \|o\|/, edit.new_text)
|
|
39
|
+
assert_match(/o\.do_first_thing/, edit.new_text)
|
|
40
|
+
assert_match(/o\.do_second_thing/, edit.new_text)
|
|
41
|
+
assert_match(/o\.do_third_thing/, edit.new_text)
|
|
42
|
+
assert_match(/end/, edit.new_text)
|
|
43
|
+
refute_match(/\bobj\b(?!\.tap)/, edit.new_text.sub("obj.tap", ""))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_preserves_method_arguments
|
|
47
|
+
source = <<~RUBY
|
|
48
|
+
def setup
|
|
49
|
+
user.assign_role(:admin)
|
|
50
|
+
user.set_name("Alice")
|
|
51
|
+
user
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
actions = code_actions_for(source, line: 0)
|
|
56
|
+
action = find_action(actions, "Convert to tap")
|
|
57
|
+
refute_nil action
|
|
58
|
+
|
|
59
|
+
edit = single_edit(action)
|
|
60
|
+
assert_match(/user\.tap do \|o\|/, edit.new_text)
|
|
61
|
+
assert_match(/o\.assign_role\(:admin\)/, edit.new_text)
|
|
62
|
+
assert_match(/o\.set_name\("Alice"\)/, edit.new_text)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_preserves_indentation
|
|
66
|
+
source = <<~RUBY
|
|
67
|
+
class Builder
|
|
68
|
+
def build
|
|
69
|
+
obj.step_one
|
|
70
|
+
obj.step_two
|
|
71
|
+
obj
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
RUBY
|
|
75
|
+
|
|
76
|
+
actions = code_actions_for(source, line: 1)
|
|
77
|
+
action = find_action(actions, "Convert to tap")
|
|
78
|
+
refute_nil action
|
|
79
|
+
|
|
80
|
+
edit = single_edit(action)
|
|
81
|
+
# The body range starts at the method body's indentation (4 spaces).
|
|
82
|
+
assert_match(/\A obj\.tap do \|o\|/, edit.new_text)
|
|
83
|
+
assert_match(/ o\.step_one/, edit.new_text)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ── negative cases ─────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def test_does_not_offer_when_last_statement_is_not_bare_receiver
|
|
89
|
+
source = <<~RUBY
|
|
90
|
+
def do_something
|
|
91
|
+
obj.step_one
|
|
92
|
+
obj.step_two
|
|
93
|
+
obj.result
|
|
94
|
+
end
|
|
95
|
+
RUBY
|
|
96
|
+
|
|
97
|
+
# last statement is a call on obj, not a bare variable read
|
|
98
|
+
actions = code_actions_for(source, line: 0)
|
|
99
|
+
assert_nil find_action(actions, "Convert to tap")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_does_not_offer_when_receivers_differ
|
|
103
|
+
source = <<~RUBY
|
|
104
|
+
def do_something
|
|
105
|
+
foo.step_one
|
|
106
|
+
bar.step_two
|
|
107
|
+
foo
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
|
|
111
|
+
actions = code_actions_for(source, line: 0)
|
|
112
|
+
assert_nil find_action(actions, "Convert to tap")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_does_not_offer_for_single_statement_method
|
|
116
|
+
source = <<~RUBY
|
|
117
|
+
def do_something
|
|
118
|
+
obj
|
|
119
|
+
end
|
|
120
|
+
RUBY
|
|
121
|
+
|
|
122
|
+
actions = code_actions_for(source, line: 0)
|
|
123
|
+
assert_nil find_action(actions, "Convert to tap")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_does_not_offer_outside_a_method
|
|
127
|
+
source = <<~RUBY
|
|
128
|
+
obj.step_one
|
|
129
|
+
obj.step_two
|
|
130
|
+
obj
|
|
131
|
+
RUBY
|
|
132
|
+
|
|
133
|
+
actions = code_actions_for(source, line: 0)
|
|
134
|
+
assert_nil find_action(actions, "Convert to tap")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ── resilience ─────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def test_does_not_raise_on_empty_source
|
|
140
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -62,29 +62,6 @@ module RubyLsp
|
|
|
62
62
|
assert_nil find_action(actions, /Inline variable/)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# ===========================================================================
|
|
66
|
-
# 2. Extract local variable
|
|
67
|
-
# ===========================================================================
|
|
68
|
-
|
|
69
|
-
def test_extract_local_variable_inserts_assignment_above
|
|
70
|
-
source = "user.full_name.upcase\n"
|
|
71
|
-
actions = code_actions_for(source, line: 0)
|
|
72
|
-
action = find_action(actions, "Extract local variable")
|
|
73
|
-
refute_nil action
|
|
74
|
-
|
|
75
|
-
edits = all_edits(action)
|
|
76
|
-
assert_equal 2, edits.size
|
|
77
|
-
|
|
78
|
-
insert_edit = edits.find { |e| e.new_text.include?("variable =") }
|
|
79
|
-
replace_edit = edits.find { |e| e.new_text == "variable" }
|
|
80
|
-
|
|
81
|
-
refute_nil insert_edit
|
|
82
|
-
refute_nil replace_edit
|
|
83
|
-
|
|
84
|
-
assert_match(/variable = user\.full_name\.upcase/, insert_edit.new_text)
|
|
85
|
-
assert_equal 0, insert_edit.range.start.line
|
|
86
|
-
end
|
|
87
|
-
|
|
88
65
|
# ---------------------------------------------------------------------------
|
|
89
66
|
# Resilience
|
|
90
67
|
# ---------------------------------------------------------------------------
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-lsp-refactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aboobacker MK
|
|
@@ -104,10 +104,12 @@ files:
|
|
|
104
104
|
- lib/ruby_lsp/ruby_lsp_refactor/addon.rb
|
|
105
105
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/accessor_listener.rb
|
|
106
106
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb
|
|
107
|
-
- lib/ruby_lsp/ruby_lsp_refactor/listeners/block_style_listener.rb
|
|
108
107
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb
|
|
109
108
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/constant_listener.rb
|
|
109
|
+
- lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb
|
|
110
110
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/enumerable_listener.rb
|
|
111
|
+
- lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb
|
|
112
|
+
- lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb
|
|
111
113
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb
|
|
112
114
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/logical_operator_listener.rb
|
|
113
115
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb
|
|
@@ -118,16 +120,19 @@ files:
|
|
|
118
120
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/string_freeze_listener.rb
|
|
119
121
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb
|
|
120
122
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb
|
|
123
|
+
- lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb
|
|
121
124
|
- lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb
|
|
122
125
|
- lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb
|
|
123
126
|
- lib/ruby_lsp/test_helper.rb
|
|
124
127
|
- sig/ruby/lsp/refactor.rbs
|
|
125
128
|
- test/ruby_lsp_refactor/accessor_listener_test.rb
|
|
126
129
|
- test/ruby_lsp_refactor/array_listener_test.rb
|
|
127
|
-
- test/ruby_lsp_refactor/block_style_listener_test.rb
|
|
128
130
|
- test/ruby_lsp_refactor/conditional_listener_test.rb
|
|
129
131
|
- test/ruby_lsp_refactor/constant_listener_test.rb
|
|
132
|
+
- test/ruby_lsp_refactor/early_return_listener_test.rb
|
|
130
133
|
- test/ruby_lsp_refactor/enumerable_listener_test.rb
|
|
134
|
+
- test/ruby_lsp_refactor/extract_include_file_listener_test.rb
|
|
135
|
+
- test/ruby_lsp_refactor/extract_predicate_listener_test.rb
|
|
131
136
|
- test/ruby_lsp_refactor/hash_listener_test.rb
|
|
132
137
|
- test/ruby_lsp_refactor/logical_operator_listener_test.rb
|
|
133
138
|
- test/ruby_lsp_refactor/method_listener_test.rb
|
|
@@ -138,6 +143,7 @@ files:
|
|
|
138
143
|
- test/ruby_lsp_refactor/string_freeze_listener_test.rb
|
|
139
144
|
- test/ruby_lsp_refactor/string_listener_test.rb
|
|
140
145
|
- test/ruby_lsp_refactor/super_listener_test.rb
|
|
146
|
+
- test/ruby_lsp_refactor/tap_listener_test.rb
|
|
141
147
|
- test/ruby_lsp_refactor/variable_listener_test.rb
|
|
142
148
|
homepage: https://github.com/tachyons/ruby-lsp-refactor
|
|
143
149
|
licenses:
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../support/node_helpers"
|
|
4
|
-
|
|
5
|
-
module RubyLsp
|
|
6
|
-
module Refactor
|
|
7
|
-
# Emits block-style toggle actions on any CallNode whose block is a
|
|
8
|
-
# BlockNode (i.e. not a bare proc/lambda literal).
|
|
9
|
-
#
|
|
10
|
-
# Emitted actions
|
|
11
|
-
# ───────────────
|
|
12
|
-
# 1. Convert to do…end
|
|
13
|
-
# receiver.method { |x| body }
|
|
14
|
-
# →
|
|
15
|
-
# receiver.method do |x|
|
|
16
|
-
# body
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# 2. Convert to brace block
|
|
20
|
-
# receiver.method do |x|
|
|
21
|
-
# body
|
|
22
|
-
# end
|
|
23
|
-
# →
|
|
24
|
-
# receiver.method { |x| body }
|
|
25
|
-
#
|
|
26
|
-
# Convention: multi-statement brace blocks are always expanded to do…end.
|
|
27
|
-
# Single-statement do…end blocks are collapsed to brace style.
|
|
28
|
-
class BlockStyleListener
|
|
29
|
-
include RubyLsp::Requests::Support::Common
|
|
30
|
-
include Support::NodeHelpers
|
|
31
|
-
|
|
32
|
-
def initialize(response_builder, node_context, dispatcher)
|
|
33
|
-
@response_builder = response_builder
|
|
34
|
-
@node_context = node_context
|
|
35
|
-
|
|
36
|
-
dispatcher.register(self, :on_call_node_enter)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def on_call_node_enter(node)
|
|
40
|
-
return unless node_covers_cursor?(node)
|
|
41
|
-
|
|
42
|
-
block = node.block
|
|
43
|
-
return unless block.is_a?(Prism::BlockNode)
|
|
44
|
-
|
|
45
|
-
if brace_block?(block)
|
|
46
|
-
emit_to_do_end(node, block)
|
|
47
|
-
elsif single_statement_body?(block)
|
|
48
|
-
emit_to_brace(node, block)
|
|
49
|
-
end
|
|
50
|
-
rescue StandardError
|
|
51
|
-
nil
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def brace_block?(block)
|
|
57
|
-
block.opening_loc.slice == "{"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def single_statement_body?(block)
|
|
61
|
-
block.body&.body&.length == 1
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# ── brace → do…end ──────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
def emit_to_do_end(call_node, block)
|
|
67
|
-
indent = indent_for(call_node)
|
|
68
|
-
params_src = params_string(block)
|
|
69
|
-
body_lines = block.body.body.map { |s| "#{indent} #{s.location.slice.strip}" }.join("\n")
|
|
70
|
-
|
|
71
|
-
new_block = " do#{params_src}\n#{body_lines}\n#{indent}end"
|
|
72
|
-
new_text = call_without_block(call_node) + new_block
|
|
73
|
-
|
|
74
|
-
@response_builder << Interface::CodeAction.new(
|
|
75
|
-
title: "Convert to do…end block",
|
|
76
|
-
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
77
|
-
edit: single_edit_workspace_edit(call_node, new_text)
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# ── do…end → brace ──────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
def emit_to_brace(call_node, block)
|
|
84
|
-
params_src = params_string(block)
|
|
85
|
-
body_src = block.body.body.first.location.slice.strip
|
|
86
|
-
|
|
87
|
-
new_block = " {#{params_src} #{body_src} }"
|
|
88
|
-
new_text = call_without_block(call_node) + new_block
|
|
89
|
-
|
|
90
|
-
@response_builder << Interface::CodeAction.new(
|
|
91
|
-
title: "Convert to brace block",
|
|
92
|
-
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
93
|
-
edit: single_edit_workspace_edit(call_node, new_text)
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# ── helpers ──────────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
# Returns the block parameters string including pipes, e.g. " |x, y|",
|
|
100
|
-
# or an empty string when the block takes no parameters.
|
|
101
|
-
def params_string(block)
|
|
102
|
-
return "" unless block.parameters
|
|
103
|
-
|
|
104
|
-
" #{block.parameters.location.slice}"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Returns the source of the call node up to (but not including) the block.
|
|
108
|
-
# Works by slicing from the call's start to the block's start.
|
|
109
|
-
def call_without_block(call_node)
|
|
110
|
-
call_src = call_node.location.slice
|
|
111
|
-
block_src = call_node.block.location.slice
|
|
112
|
-
# Remove the block suffix (and any whitespace before it) from the call.
|
|
113
|
-
call_src[0, call_src.length - block_src.length].rstrip
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,98 +0,0 @@
|
|
|
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 BlockStyleListenerTest < 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
|
-
# ── brace → do…end ────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
def test_converts_brace_block_to_do_end
|
|
24
|
-
source = "users.each { |u| u.activate! }\n"
|
|
25
|
-
actions = code_actions_for(source, line: 0)
|
|
26
|
-
action = find_action(actions, "Convert to do…end block")
|
|
27
|
-
refute_nil action
|
|
28
|
-
|
|
29
|
-
edit = single_edit(action)
|
|
30
|
-
assert_match(/do \|u\|/, edit.new_text)
|
|
31
|
-
assert_match(/u\.activate!/, edit.new_text)
|
|
32
|
-
assert_match(/end/, edit.new_text)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def test_brace_to_do_end_preserves_indentation
|
|
36
|
-
source = " users.each { |u| u.activate! }\n"
|
|
37
|
-
actions = code_actions_for(source, line: 0)
|
|
38
|
-
action = find_action(actions, "Convert to do…end block")
|
|
39
|
-
refute_nil action
|
|
40
|
-
|
|
41
|
-
edit = single_edit(action)
|
|
42
|
-
# The edit replaces the node range which starts at column 2, so
|
|
43
|
-
# new_text begins at the call itself (no leading spaces).
|
|
44
|
-
assert_match(/\Ausers\.each do \|u\|/, edit.new_text)
|
|
45
|
-
# Body is indented 2 (node column) + 2 (block body) = 4 spaces.
|
|
46
|
-
assert_match(/^ u\.activate!/, edit.new_text)
|
|
47
|
-
# Closing end is at the node's own indentation level (2 spaces).
|
|
48
|
-
assert_match(/^ end$/, edit.new_text)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def test_brace_to_do_end_without_params
|
|
52
|
-
source = "3.times { puts 'hi' }\n"
|
|
53
|
-
actions = code_actions_for(source, line: 0)
|
|
54
|
-
action = find_action(actions, "Convert to do…end block")
|
|
55
|
-
refute_nil action
|
|
56
|
-
|
|
57
|
-
edit = single_edit(action)
|
|
58
|
-
assert_match(/do\n/, edit.new_text)
|
|
59
|
-
refute_match(/\|/, edit.new_text)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# ── do…end → brace ────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
def test_converts_do_end_block_to_brace
|
|
65
|
-
source = <<~RUBY
|
|
66
|
-
users.each do |u|
|
|
67
|
-
u.activate!
|
|
68
|
-
end
|
|
69
|
-
RUBY
|
|
70
|
-
|
|
71
|
-
actions = code_actions_for(source, line: 0)
|
|
72
|
-
action = find_action(actions, "Convert to brace block")
|
|
73
|
-
refute_nil action
|
|
74
|
-
|
|
75
|
-
edit = single_edit(action)
|
|
76
|
-
assert_match(/\{ \|u\| u\.activate! \}/, edit.new_text)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def test_does_not_offer_brace_for_multi_statement_do_end
|
|
80
|
-
source = <<~RUBY
|
|
81
|
-
users.each do |u|
|
|
82
|
-
u.activate!
|
|
83
|
-
u.notify!
|
|
84
|
-
end
|
|
85
|
-
RUBY
|
|
86
|
-
|
|
87
|
-
actions = code_actions_for(source, line: 0)
|
|
88
|
-
assert_nil find_action(actions, "Convert to brace block")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# ── resilience ─────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
def test_does_not_raise_on_empty_source
|
|
94
|
-
assert_silent { code_actions_for("", line: 0) }
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|