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.
@@ -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. 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
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
- # 3. Convert to keyword arguments
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
- # 4. Extract to let (RSpec)
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.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