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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Offers "Wrap in freeze" on any unfrozen string literal, and
|
|
8
|
+
# "Remove freeze" on a string that already calls .freeze.
|
|
9
|
+
#
|
|
10
|
+
# Emitted actions
|
|
11
|
+
# ───────────────
|
|
12
|
+
# 1. Wrap in freeze
|
|
13
|
+
# "hello" → "hello".freeze
|
|
14
|
+
#
|
|
15
|
+
# 2. Remove freeze
|
|
16
|
+
# "hello".freeze → "hello"
|
|
17
|
+
class StringFreezeListener
|
|
18
|
+
include RubyLsp::Requests::Support::Common
|
|
19
|
+
include Support::NodeHelpers
|
|
20
|
+
|
|
21
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
22
|
+
@response_builder = response_builder
|
|
23
|
+
@node_context = node_context
|
|
24
|
+
|
|
25
|
+
# Track start offsets of string nodes that are already receivers of
|
|
26
|
+
# .freeze so on_string_node_enter can skip them. Populated during
|
|
27
|
+
# on_call_node_enter which fires for the outer CallNode first.
|
|
28
|
+
@frozen_string_offsets = {}
|
|
29
|
+
|
|
30
|
+
dispatcher.register(self, :on_call_node_enter, :on_string_node_enter)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Fires for every CallNode — including `.freeze` calls.
|
|
34
|
+
# We register this before on_string_node_enter so the offset set is
|
|
35
|
+
# populated before the inner StringNode callback fires.
|
|
36
|
+
def on_call_node_enter(node)
|
|
37
|
+
return unless node.name == :freeze
|
|
38
|
+
return unless node.receiver.is_a?(Prism::StringNode)
|
|
39
|
+
return unless node.arguments.nil?
|
|
40
|
+
|
|
41
|
+
# Mark the receiver string so on_string_node_enter skips it.
|
|
42
|
+
@frozen_string_offsets[node.receiver.location.start_offset] = true
|
|
43
|
+
|
|
44
|
+
return unless node_covers_cursor?(node)
|
|
45
|
+
|
|
46
|
+
emit_remove_freeze(node)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Offer "Wrap in freeze" for plain string literals that are not already
|
|
52
|
+
# the receiver of a .freeze call.
|
|
53
|
+
def on_string_node_enter(node)
|
|
54
|
+
return unless node_covers_cursor?(node)
|
|
55
|
+
return if @frozen_string_offsets.key?(node.location.start_offset)
|
|
56
|
+
|
|
57
|
+
emit_wrap_freeze(node)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def emit_wrap_freeze(node)
|
|
65
|
+
str_src = node.location.slice
|
|
66
|
+
new_text = "#{str_src}.freeze"
|
|
67
|
+
|
|
68
|
+
@response_builder << Interface::CodeAction.new(
|
|
69
|
+
title: "Wrap in freeze",
|
|
70
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
71
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit_remove_freeze(node)
|
|
76
|
+
str_src = node.receiver.location.slice
|
|
77
|
+
|
|
78
|
+
@response_builder << Interface::CodeAction.new(
|
|
79
|
+
title: "Remove freeze",
|
|
80
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
81
|
+
edit: single_edit_workspace_edit(node, str_src)
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -52,8 +52,8 @@ module RubyLsp
|
|
|
52
52
|
|
|
53
53
|
@response_builder << Interface::CodeAction.new(
|
|
54
54
|
title: "Convert to interpolated string",
|
|
55
|
-
kind:
|
|
56
|
-
edit:
|
|
55
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
56
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
57
57
|
)
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Converts a bare `super` (ForwardingSuperNode) inside a def that has
|
|
8
|
+
# parameters into an explicit `super(param1, param2, ...)`.
|
|
9
|
+
#
|
|
10
|
+
# Input (cursor on `super`):
|
|
11
|
+
# def initialize(name, age)
|
|
12
|
+
# super
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Output:
|
|
16
|
+
# def initialize(name, age)
|
|
17
|
+
# super(name, age)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Bare `super` forwards all arguments implicitly; making them explicit
|
|
21
|
+
# is safer when the method signature changes over time.
|
|
22
|
+
class SuperListener
|
|
23
|
+
include RubyLsp::Requests::Support::Common
|
|
24
|
+
include Support::NodeHelpers
|
|
25
|
+
|
|
26
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
27
|
+
@response_builder = response_builder
|
|
28
|
+
@node_context = node_context
|
|
29
|
+
|
|
30
|
+
@current_def = nil
|
|
31
|
+
|
|
32
|
+
dispatcher.register(
|
|
33
|
+
self,
|
|
34
|
+
:on_def_node_enter,
|
|
35
|
+
:on_def_node_leave,
|
|
36
|
+
:on_forwarding_super_node_enter
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_def_node_enter(node)
|
|
41
|
+
@current_def = node
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def on_def_node_leave(_node)
|
|
45
|
+
@current_def = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_forwarding_super_node_enter(node)
|
|
49
|
+
return unless node_covers_cursor?(node)
|
|
50
|
+
return unless @current_def
|
|
51
|
+
return unless has_params?(@current_def)
|
|
52
|
+
|
|
53
|
+
emit_explicit_super(node, @current_def)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def has_params?(def_node)
|
|
61
|
+
params = def_node.parameters
|
|
62
|
+
return false unless params
|
|
63
|
+
|
|
64
|
+
params.requireds.any? || params.optionals.any? || params.keywords.any?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emit_explicit_super(super_node, def_node)
|
|
68
|
+
param_names = collect_param_names(def_node.parameters)
|
|
69
|
+
new_text = "#{indent_for(super_node)}super(#{param_names.join(", ")})"
|
|
70
|
+
|
|
71
|
+
@response_builder << Interface::CodeAction.new(
|
|
72
|
+
title: "Convert to explicit super(#{param_names.join(", ")})",
|
|
73
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
74
|
+
edit: single_edit_workspace_edit(super_node, new_text)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collect_param_names(params_node)
|
|
79
|
+
names = []
|
|
80
|
+
params_node.requireds.each do |p|
|
|
81
|
+
names << p.name.to_s if p.respond_to?(:name)
|
|
82
|
+
end
|
|
83
|
+
params_node.optionals.each do |p|
|
|
84
|
+
names << p.name.to_s if p.respond_to?(:name)
|
|
85
|
+
end
|
|
86
|
+
params_node.keywords.each do |p|
|
|
87
|
+
# keyword params: `name:` — pass as `name: name`
|
|
88
|
+
kw_name = p.name.to_s.delete_suffix(":")
|
|
89
|
+
names << "#{kw_name}: #{kw_name}"
|
|
90
|
+
end
|
|
91
|
+
names
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -41,7 +41,7 @@ module RubyLsp
|
|
|
41
41
|
:on_local_variable_write_node_enter,
|
|
42
42
|
:on_local_variable_read_node_enter,
|
|
43
43
|
:on_call_node_enter,
|
|
44
|
-
:on_program_node_leave
|
|
44
|
+
:on_program_node_leave
|
|
45
45
|
)
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -89,15 +89,15 @@ module RubyLsp
|
|
|
89
89
|
|
|
90
90
|
@read_nodes[write_node.name].each do |read_node|
|
|
91
91
|
edits << Interface::TextEdit.new(
|
|
92
|
-
range:
|
|
93
|
-
new_text: rhs_text
|
|
92
|
+
range: node_to_lsp_range(read_node),
|
|
93
|
+
new_text: rhs_text
|
|
94
94
|
)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
@response_builder << Interface::CodeAction.new(
|
|
98
98
|
title: "Inline variable '#{write_node.name}'",
|
|
99
|
-
kind:
|
|
100
|
-
edit:
|
|
99
|
+
kind: Constant::CodeActionKind::REFACTOR_INLINE,
|
|
100
|
+
edit: multi_edit_workspace_edit(edits)
|
|
101
101
|
)
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -113,20 +113,20 @@ module RubyLsp
|
|
|
113
113
|
insert_edit = Interface::TextEdit.new(
|
|
114
114
|
range: Interface::Range.new(
|
|
115
115
|
start: Interface::Position.new(line: insert_line, character: 0),
|
|
116
|
-
end:
|
|
116
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
117
117
|
),
|
|
118
|
-
new_text: "#{indent}variable = #{expr_src}\n"
|
|
118
|
+
new_text: "#{indent}variable = #{expr_src}\n"
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
replace_edit = Interface::TextEdit.new(
|
|
122
|
-
range:
|
|
123
|
-
new_text: "variable"
|
|
122
|
+
range: node_to_lsp_range(node),
|
|
123
|
+
new_text: "variable"
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
@response_builder << Interface::CodeAction.new(
|
|
127
127
|
title: "Extract local variable",
|
|
128
|
-
kind:
|
|
129
|
-
edit:
|
|
128
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
129
|
+
edit: multi_edit_workspace_edit([insert_edit, replace_edit])
|
|
130
130
|
)
|
|
131
131
|
end
|
|
132
132
|
end
|
|
@@ -18,13 +18,13 @@ module RubyLsp
|
|
|
18
18
|
loc = node.location
|
|
19
19
|
Interface::Range.new(
|
|
20
20
|
start: Interface::Position.new(
|
|
21
|
-
line:
|
|
22
|
-
character: loc.start_column
|
|
21
|
+
line: loc.start_line - 1,
|
|
22
|
+
character: loc.start_column
|
|
23
23
|
),
|
|
24
24
|
end: Interface::Position.new(
|
|
25
|
-
line:
|
|
26
|
-
character: loc.end_column
|
|
27
|
-
)
|
|
25
|
+
line: loc.end_line - 1,
|
|
26
|
+
character: loc.end_column
|
|
27
|
+
)
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -56,9 +56,9 @@ module RubyLsp
|
|
|
56
56
|
Interface::WorkspaceEdit.new(
|
|
57
57
|
changes: {
|
|
58
58
|
@node_context.uri => [
|
|
59
|
-
Interface::TextEdit.new(range: node_to_lsp_range(node), new_text: new_text)
|
|
60
|
-
]
|
|
61
|
-
}
|
|
59
|
+
Interface::TextEdit.new(range: node_to_lsp_range(node), new_text: new_text)
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
62
|
)
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ module RubyLsp
|
|
|
68
68
|
# @return [Interface::WorkspaceEdit]
|
|
69
69
|
def multi_edit_workspace_edit(edits)
|
|
70
70
|
Interface::WorkspaceEdit.new(
|
|
71
|
-
changes: { @node_context.uri => edits }
|
|
71
|
+
changes: { @node_context.uri => edits }
|
|
72
72
|
)
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -81,10 +81,10 @@ module RubyLsp
|
|
|
81
81
|
line = node.location.start_line - 1
|
|
82
82
|
Interface::TextEdit.new(
|
|
83
83
|
range: Interface::Range.new(
|
|
84
|
-
start: Interface::Position.new(line: line,
|
|
85
|
-
end:
|
|
84
|
+
start: Interface::Position.new(line: line, character: 0),
|
|
85
|
+
end: Interface::Position.new(line: line + 1, character: 0)
|
|
86
86
|
),
|
|
87
|
-
new_text: ""
|
|
87
|
+
new_text: ""
|
|
88
88
|
)
|
|
89
89
|
end
|
|
90
90
|
|
data/lib/ruby_lsp/test_helper.rb
CHANGED
|
@@ -31,16 +31,16 @@ module RubyLsp
|
|
|
31
31
|
uri = URI::Generic.from_path(path: "/test/fixture.rb")
|
|
32
32
|
global_state = RubyLsp::GlobalState.new
|
|
33
33
|
document = RubyLsp::RubyDocument.new(
|
|
34
|
-
source:
|
|
35
|
-
version:
|
|
36
|
-
uri:
|
|
37
|
-
global_state: global_state
|
|
34
|
+
source: source,
|
|
35
|
+
version: 1,
|
|
36
|
+
uri: uri,
|
|
37
|
+
global_state: global_state
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
# LSP range hash — same shape the real server passes to CodeActions.
|
|
41
41
|
range = {
|
|
42
42
|
start: { line: line, character: char },
|
|
43
|
-
end:
|
|
43
|
+
end: { line: line, character: char }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
RubyLsp::Refactor::Addon.refactor_actions_for(document, range)
|
|
@@ -0,0 +1,91 @@
|
|
|
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 AccessorListenerTest < 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 test_collapses_attr_reader_and_writer_into_attr_accessor
|
|
16
|
+
source = <<~RUBY
|
|
17
|
+
class User
|
|
18
|
+
attr_reader :name
|
|
19
|
+
|
|
20
|
+
def name=(val)
|
|
21
|
+
@name = val
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
RUBY
|
|
25
|
+
|
|
26
|
+
# Cursor on the attr_reader line
|
|
27
|
+
actions = code_actions_for(source, line: 1)
|
|
28
|
+
action = find_action(actions, /attr_accessor :name/)
|
|
29
|
+
refute_nil action
|
|
30
|
+
|
|
31
|
+
edits = action.edit.changes.values.flatten
|
|
32
|
+
assert_equal 2, edits.size
|
|
33
|
+
|
|
34
|
+
replace_edit = edits.find { |e| e.new_text.include?("attr_accessor") }
|
|
35
|
+
delete_edit = edits.find { |e| e.new_text == "" }
|
|
36
|
+
|
|
37
|
+
refute_nil replace_edit
|
|
38
|
+
refute_nil delete_edit
|
|
39
|
+
assert_match(/attr_accessor :name/, replace_edit.new_text)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_offers_action_from_writer_def_line_too
|
|
43
|
+
source = <<~RUBY
|
|
44
|
+
class User
|
|
45
|
+
attr_reader :name
|
|
46
|
+
|
|
47
|
+
def name=(val)
|
|
48
|
+
@name = val
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
|
|
53
|
+
# Cursor on the def name= line
|
|
54
|
+
actions = code_actions_for(source, line: 3)
|
|
55
|
+
action = find_action(actions, /attr_accessor :name/)
|
|
56
|
+
refute_nil action
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_does_not_offer_when_no_matching_writer
|
|
60
|
+
source = <<~RUBY
|
|
61
|
+
class User
|
|
62
|
+
attr_reader :name
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
actions = code_actions_for(source, line: 1)
|
|
67
|
+
assert_nil find_action(actions, /attr_accessor/)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_does_not_offer_for_non_canonical_writer
|
|
71
|
+
# Writer has extra logic — not a simple passthrough
|
|
72
|
+
source = <<~RUBY
|
|
73
|
+
class User
|
|
74
|
+
attr_reader :name
|
|
75
|
+
|
|
76
|
+
def name=(val)
|
|
77
|
+
@name = val.strip
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
|
|
82
|
+
actions = code_actions_for(source, line: 1)
|
|
83
|
+
assert_nil find_action(actions, /attr_accessor/)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_does_not_raise_on_empty_source
|
|
87
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
|
@@ -0,0 +1,68 @@
|
|
|
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 ConstantListenerTest < 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 test_extracts_integer_literal_to_constant
|
|
16
|
+
source = <<~RUBY
|
|
17
|
+
class Processor
|
|
18
|
+
def run
|
|
19
|
+
items.first(100)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
RUBY
|
|
23
|
+
|
|
24
|
+
actions = code_actions_for(source, line: 2)
|
|
25
|
+
action = find_action(actions, "Extract constant")
|
|
26
|
+
refute_nil action
|
|
27
|
+
|
|
28
|
+
edits = action.edit.changes.values.flatten
|
|
29
|
+
assert_equal 2, edits.size
|
|
30
|
+
|
|
31
|
+
insert_edit = edits.find { |e| e.new_text.include?("EXTRACTED_CONSTANT") && e.new_text.include?("=") }
|
|
32
|
+
replace_edit = edits.find { |e| e.new_text == "EXTRACTED_CONSTANT" }
|
|
33
|
+
|
|
34
|
+
refute_nil insert_edit
|
|
35
|
+
refute_nil replace_edit
|
|
36
|
+
assert_match(/EXTRACTED_CONSTANT = 100/, insert_edit.new_text)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_extracts_string_literal_to_constant
|
|
40
|
+
source = <<~RUBY
|
|
41
|
+
class Mailer
|
|
42
|
+
def subject
|
|
43
|
+
"Welcome to the app"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
RUBY
|
|
47
|
+
|
|
48
|
+
actions = code_actions_for(source, line: 2)
|
|
49
|
+
action = find_action(actions, "Extract constant")
|
|
50
|
+
refute_nil action
|
|
51
|
+
|
|
52
|
+
edits = action.edit.changes.values.flatten
|
|
53
|
+
insert_edit = edits.find { |e| e.new_text.include?("EXTRACTED_CONSTANT") && e.new_text.include?("=") }
|
|
54
|
+
assert_match(/EXTRACTED_CONSTANT = "Welcome to the app"/, insert_edit.new_text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_does_not_offer_outside_class
|
|
58
|
+
source = "items.first(100)\n"
|
|
59
|
+
actions = code_actions_for(source, line: 0)
|
|
60
|
+
assert_nil find_action(actions, "Extract constant")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_does_not_raise_on_empty_source
|
|
64
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
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 EnumerableListenerTest < 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
|
+
# ── map + flatten → flat_map ───────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
def test_converts_map_flatten_1_to_flat_map
|
|
24
|
+
source = "items.map { |i| i.tags }.flatten(1)\n"
|
|
25
|
+
actions = code_actions_for(source, line: 0)
|
|
26
|
+
action = find_action(actions, "Convert to .flat_map")
|
|
27
|
+
refute_nil action
|
|
28
|
+
|
|
29
|
+
edit = single_edit(action)
|
|
30
|
+
assert_equal "items.flat_map { |i| i.tags }", edit.new_text.strip
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_converts_map_flatten_no_arg_to_flat_map
|
|
34
|
+
source = "items.map { |i| i.tags }.flatten\n"
|
|
35
|
+
actions = code_actions_for(source, line: 0)
|
|
36
|
+
action = find_action(actions, "Convert to .flat_map")
|
|
37
|
+
refute_nil action
|
|
38
|
+
|
|
39
|
+
edit = single_edit(action)
|
|
40
|
+
assert_equal "items.flat_map { |i| i.tags }", edit.new_text.strip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_does_not_offer_flat_map_for_flatten_2
|
|
44
|
+
source = "items.map { |i| i.tags }.flatten(2)\n"
|
|
45
|
+
actions = code_actions_for(source, line: 0)
|
|
46
|
+
assert_nil find_action(actions, "Convert to .flat_map")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── select + first → find ─────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def test_converts_select_first_to_find
|
|
52
|
+
source = "users.select { |u| u.admin? }.first\n"
|
|
53
|
+
actions = code_actions_for(source, line: 0)
|
|
54
|
+
action = find_action(actions, "Convert to .find")
|
|
55
|
+
refute_nil action
|
|
56
|
+
|
|
57
|
+
edit = single_edit(action)
|
|
58
|
+
assert_equal "users.find { |u| u.admin? }", edit.new_text.strip
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ── map + compact → filter_map ────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def test_converts_map_compact_to_filter_map
|
|
64
|
+
source = "items.map { |i| i.value }.compact\n"
|
|
65
|
+
actions = code_actions_for(source, line: 0)
|
|
66
|
+
action = find_action(actions, "Convert to .filter_map")
|
|
67
|
+
refute_nil action
|
|
68
|
+
|
|
69
|
+
edit = single_edit(action)
|
|
70
|
+
assert_equal "items.filter_map { |i| i.value }", edit.new_text.strip
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── resilience ─────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def test_does_not_raise_on_empty_source
|
|
76
|
+
assert_silent { code_actions_for("", line: 0) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|