From c12a73cac6aa4420ce138e72f0c8318e6bf31034 Mon Sep 17 00:00:00 2001 From: Nathan Daniels Date: Mon, 28 Jul 2025 21:10:01 -0400 Subject: [PATCH 1/3] fix: only group specs if the `context` or `describe` call has a block --- lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb | 4 +++- lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb b/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb index a5c999d..2c15070 100644 --- a/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb +++ b/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb @@ -55,9 +55,11 @@ def log_message(message) puts "[#{self.class}]: #{message}" end + # A node is valid if it has a block and the receiver is RSpec (or nil) #: (Prism::CallNode) -> bool def valid_group?(node) - !(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec")) + return false if node.block.nil? + node.receiver.nil? || node.receiver&.slice == "RSpec" end #: (Prism::CallNode) -> String diff --git a/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb b/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb index e349ab6..0a2534a 100644 --- a/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb +++ b/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb @@ -30,6 +30,7 @@ def on_call_node_enter(node) case node.message when "describe", "context" + return unless valid_group?(node) handle_describe(node) when "it", "specify", "example" handle_example(node) @@ -116,9 +117,11 @@ def find_parent_test_group @group_stack.last end + # A node is valid if it has a block and the receiver is RSpec (or nil) #: (Prism::CallNode) -> bool def valid_group?(node) - !(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec")) + return false if node.block.nil? + node.receiver.nil? || node.receiver&.slice == "RSpec" end #: (Prism::CallNode) -> String From dfce072fe04ab50105d8dea537ea02012469d4a7 Mon Sep 17 00:00:00 2001 From: Nathan Daniels Date: Mon, 8 Sep 2025 09:45:22 -0400 Subject: [PATCH 2/3] Add new specs --- lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb | 2 + spec/code_lens_spec.rb | 116 ++++++++++++++++++ spec/test_discovery_spec.rb | 108 ++++++++++++++++ 3 files changed, 226 insertions(+) diff --git a/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb b/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb index 0a2534a..41ff7ca 100644 --- a/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb +++ b/lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb @@ -31,6 +31,7 @@ def on_call_node_enter(node) case node.message when "describe", "context" return unless valid_group?(node) + handle_describe(node) when "it", "specify", "example" handle_example(node) @@ -121,6 +122,7 @@ def find_parent_test_group #: (Prism::CallNode) -> bool def valid_group?(node) return false if node.block.nil? + node.receiver.nil? || node.receiver&.slice == "RSpec" end diff --git a/spec/code_lens_spec.rb b/spec/code_lens_spec.rb index e31f156..f323062 100644 --- a/spec/code_lens_spec.rb +++ b/spec/code_lens_spec.rb @@ -229,6 +229,122 @@ def dummy end end + it "ignores describe and context calls without blocks" do + source = <<~RUBY + RSpec.describe "Valid group with block" do + it "test in valid group" do + end + end + + # These should be ignored because they don't have blocks + RSpec.describe "Invalid group without block" + RSpec.context "Another invalid group" + + # This should also work with non-RSpec receivers + describe "Valid group without RSpec prefix" do + it "test in valid group" do + end + end + + # Invalid without block, even without RSpec prefix + describe "Invalid without block" + context "Invalid context without block" + RUBY + + with_server(source, uri) do |server, uri| + server.process_message( + { + id: 1, + method: "textDocument/codeLens", + params: { + textDocument: { uri: uri }, + position: { line: 0, character: 0 }, + }, + }, + ) + + response = pop_result(server).response + + # Should only generate code lens for the 2 valid groups (with blocks) and their children + # Each group gets 3 code lenses (run, run in terminal, debug) + # Each example gets 3 code lenses + # So: 2 groups * 3 + 2 examples * 3 = 12 total + expect(response.count).to eq(12) + + # Verify the valid groups are present + group_commands = response.select { |r| r.data[:kind] == :group } + expect(group_commands.count).to eq(6) # 2 groups * 3 commands each + + # Check that the correct groups are present + group_names = group_commands.map { |cmd| cmd.command.arguments[1] }.uniq + expect(group_names).to contain_exactly("Valid group with block", "Valid group without RSpec prefix") + + # Verify examples are present + example_commands = response.select { |r| r.data[:kind] == :example } + expect(example_commands.count).to eq(6) # 2 examples * 3 commands each + end + end + + it "handles nested groups where some lack blocks" do + source = <<~RUBY + RSpec.describe "Outer group with block" do + # Valid nested group + describe "Valid nested group" do + it "nested test" do + end + end + + # Invalid nested group (no block) - should be ignored + describe "Invalid nested group" + + # Another valid nested group + context "Valid context" do + it "context test" do + end + end + + # Invalid context (no block) - should be ignored + context "Invalid context" + end + RUBY + + with_server(source, uri) do |server, uri| + server.process_message( + { + id: 1, + method: "textDocument/codeLens", + params: { + textDocument: { uri: uri }, + position: { line: 0, character: 0 }, + }, + }, + ) + + response = pop_result(server).response + + # Should generate code lens for: + # - 1 outer group (3 commands) + # - 2 valid nested groups (2 * 3 = 6 commands) + # - 2 examples (2 * 3 = 6 commands) + # Total: 15 commands + expect(response.count).to eq(15) + + # Check that only the valid groups are present + group_commands = response.select { |r| r.data[:kind] == :group } + expect(group_commands.count).to eq(9) # 3 groups * 3 commands each + + group_names = group_commands.map { |cmd| cmd.command.arguments[1] }.uniq + expect(group_names).to contain_exactly("Outer group with block", "Valid nested group", "Valid context") + + # Verify examples are present + example_commands = response.select { |r| r.data[:kind] == :example } + expect(example_commands.count).to eq(6) # 2 examples * 3 commands each + + example_names = example_commands.map { |cmd| cmd.command.arguments[1] }.uniq + expect(example_names).to contain_exactly("nested test", "context test") + end + end + context "with a custom rspec command configured" do let(:configuration) do { diff --git a/spec/test_discovery_spec.rb b/spec/test_discovery_spec.rb index 5b59f13..bbe01e4 100644 --- a/spec/test_discovery_spec.rb +++ b/spec/test_discovery_spec.rb @@ -178,5 +178,113 @@ expect(third_example[:label]).to eq("example at ./spec/fake_spec.rb:10") end end + + it "ignores describe and context calls without blocks" do + source = <<~RUBY + RSpec.describe "Valid group with block" do + it "test in valid group" do + expect(true).to be(true) + end + end + + # These should be ignored because they don't have blocks + RSpec.describe "Invalid group without block" + RSpec.context "Another invalid group" + + # This should also work with non-RSpec receivers + describe "Valid group without RSpec prefix" do + it "test in valid group" do + expect(true).to be(true) + end + end + + # Invalid without block, even without RSpec prefix + describe "Invalid without block" + context "Invalid context without block" + RUBY + + with_server(source, uri) do |server, uri| + server.process_message( + { + id: 1, + method: "rubyLsp/discoverTests", + params: { + textDocument: { uri: uri }, + }, + }, + ) + + items = pop_result(server).response + + # Should only find 2 valid groups (the ones with blocks) + expect(items.length).to eq(2) + + first_group = items.first + expect(first_group[:label]).to eq("Valid group with block") + expect(first_group[:children].length).to eq(1) + expect(first_group[:children][0][:label]).to eq("test in valid group") + + second_group = items[1] + expect(second_group[:label]).to eq("Valid group without RSpec prefix") + expect(second_group[:children].length).to eq(1) + expect(second_group[:children][0][:label]).to eq("test in valid group") + end + end + + it "handles nested groups where some lack blocks" do + source = <<~RUBY + RSpec.describe "Outer group with block" do + # Valid nested group + describe "Valid nested group" do + it "nested test" do + expect(true).to be(true) + end + end + + # Invalid nested group (no block) - should be ignored + describe "Invalid nested group" + + # Another valid nested group + context "Valid context" do + it "context test" do + expect(true).to be(true) + end + end + + # Invalid context (no block) - should be ignored + context "Invalid context" + end + RUBY + + with_server(source, uri) do |server, uri| + server.process_message( + { + id: 1, + method: "rubyLsp/discoverTests", + params: { + textDocument: { uri: uri }, + }, + }, + ) + + items = pop_result(server).response + + expect(items.length).to eq(1) + + outer_group = items.first + expect(outer_group[:label]).to eq("Outer group with block") + # Should only have 2 children (the valid nested groups) + expect(outer_group[:children].length).to eq(2) + + nested_groups = outer_group[:children] + expect(nested_groups[0][:label]).to eq("Valid nested group") + expect(nested_groups[0][:children].length).to eq(1) + expect(nested_groups[0][:children][0][:label]).to eq("nested test") + + expect(nested_groups[1][:label]).to eq("Valid context") + expect(nested_groups[1][:children].length).to eq(1) + expect(nested_groups[1][:children][0][:label]).to eq("context test") + end + end end end From fac6128282fafc9fbe2c2c871f61908593f2a360 Mon Sep 17 00:00:00 2001 From: Nathan Daniels Date: Fri, 12 Sep 2025 09:24:31 -0400 Subject: [PATCH 3/3] Run a rubocop auto-correct pass --- lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb | 1 + spec/code_lens_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb b/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb index 2c15070..abd1399 100644 --- a/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb +++ b/lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb @@ -59,6 +59,7 @@ def log_message(message) #: (Prism::CallNode) -> bool def valid_group?(node) return false if node.block.nil? + node.receiver.nil? || node.receiver&.slice == "RSpec" end diff --git a/spec/code_lens_spec.rb b/spec/code_lens_spec.rb index f323062..b86b6a4 100644 --- a/spec/code_lens_spec.rb +++ b/spec/code_lens_spec.rb @@ -267,7 +267,7 @@ def dummy # Should only generate code lens for the 2 valid groups (with blocks) and their children # Each group gets 3 code lenses (run, run in terminal, debug) - # Each example gets 3 code lenses + # Each example gets 3 code lenses # So: 2 groups * 3 + 2 examples * 3 = 12 total expect(response.count).to eq(12) @@ -324,7 +324,7 @@ def dummy # Should generate code lens for: # - 1 outer group (3 commands) - # - 2 valid nested groups (2 * 3 = 6 commands) + # - 2 valid nested groups (2 * 3 = 6 commands) # - 2 examples (2 * 3 = 6 commands) # Total: 15 commands expect(response.count).to eq(15)