-
Notifications
You must be signed in to change notification settings - Fork 183
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
541 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
module RubyLsp | ||
module Listeners | ||
class TestStyle | ||
extend T::Sig | ||
include Requests::Support::Common | ||
|
||
ACCESS_MODIFIERS = [:public, :private, :protected].freeze | ||
DYNAMIC_REFERENCE_MARKER = "<dynamic_reference>" | ||
|
||
sig do | ||
params( | ||
response_builder: ResponseBuilders::TestItems, | ||
global_state: GlobalState, | ||
dispatcher: Prism::Dispatcher, | ||
uri: URI::Generic, | ||
).void | ||
end | ||
def initialize(response_builder, global_state, dispatcher, uri) | ||
@response_builder = response_builder | ||
@global_state = global_state | ||
@uri = uri | ||
@index = T.let(global_state.index, RubyIndexer::Index) | ||
|
||
@visibility_stack = T.let([:public], T::Array[Symbol]) | ||
@nesting = T.let([], T::Array[String]) | ||
|
||
dispatcher.register( | ||
self, | ||
:on_class_node_enter, | ||
:on_class_node_leave, | ||
:on_module_node_enter, | ||
:on_module_node_leave, | ||
:on_def_node_enter, | ||
:on_call_node_enter, | ||
:on_call_node_leave, | ||
) | ||
end | ||
|
||
sig { params(node: Prism::ClassNode).void } | ||
def on_class_node_enter(node) | ||
@visibility_stack << :public | ||
name = constant_name(node.constant_path) | ||
name ||= name_with_dynamic_reference(node.constant_path) | ||
|
||
fully_qualified_name = RubyIndexer::Index.actual_nesting(@nesting, name).join("::") | ||
|
||
attached_ancestors = begin | ||
@index.linearized_ancestors_of(fully_qualified_name) | ||
rescue RubyIndexer::Index::NonExistingNamespaceError | ||
# When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still | ||
# provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test | ||
[node.superclass&.slice].compact | ||
end | ||
|
||
if attached_ancestors.include?("Test::Unit::TestCase") || | ||
non_declarative_minitest?(attached_ancestors, fully_qualified_name) | ||
|
||
@response_builder << Requests::Support::TestItem.new( | ||
fully_qualified_name, | ||
fully_qualified_name, | ||
@uri, | ||
range_from_node(node), | ||
) | ||
end | ||
|
||
@nesting << name | ||
end | ||
|
||
sig { params(node: Prism::ModuleNode).void } | ||
def on_module_node_enter(node) | ||
@visibility_stack << :public | ||
|
||
name = constant_name(node.constant_path) | ||
name ||= name_with_dynamic_reference(node.constant_path) | ||
|
||
@nesting << name | ||
end | ||
|
||
sig { params(node: Prism::ModuleNode).void } | ||
def on_module_node_leave(node) | ||
@visibility_stack.pop | ||
@nesting.pop | ||
end | ||
|
||
sig { params(node: Prism::ClassNode).void } | ||
def on_class_node_leave(node) | ||
@visibility_stack.pop | ||
@nesting.pop | ||
end | ||
|
||
sig { params(node: Prism::DefNode).void } | ||
def on_def_node_enter(node) | ||
return if @visibility_stack.last != :public | ||
|
||
current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::") | ||
test_item = @response_builder[current_group_name] | ||
return unless test_item | ||
|
||
name = node.name.to_s | ||
return unless name.start_with?("test_") | ||
|
||
test_item << Requests::Support::TestItem.new("#{current_group_name}##{name}", name, @uri, range_from_node(node)) | ||
end | ||
|
||
sig { params(node: Prism::CallNode).void } | ||
def on_call_node_enter(node) | ||
name = node.name | ||
return unless ACCESS_MODIFIERS.include?(name) | ||
|
||
@visibility_stack << name | ||
end | ||
|
||
sig { params(node: Prism::CallNode).void } | ||
def on_call_node_leave(node) | ||
name = node.name | ||
return unless ACCESS_MODIFIERS.include?(name) | ||
return unless node.arguments&.arguments | ||
|
||
@visibility_stack.pop | ||
end | ||
|
||
private | ||
|
||
sig { params(attached_ancestors: T::Array[String], fully_qualified_name: String).returns(T::Boolean) } | ||
def non_declarative_minitest?(attached_ancestors, fully_qualified_name) | ||
return false unless attached_ancestors.include?("Minitest::Test") | ||
|
||
# We only support regular Minitest tests. The declarative syntax provided by ActiveSupport is handled by the | ||
# Rails add-on | ||
name_parts = fully_qualified_name.split("::") | ||
singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>" | ||
!@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative") | ||
rescue RubyIndexer::Index::NonExistingNamespaceError | ||
true | ||
end | ||
|
||
sig do | ||
params( | ||
node: T.any( | ||
Prism::ConstantPathNode, | ||
Prism::ConstantReadNode, | ||
Prism::ConstantPathTargetNode, | ||
Prism::CallNode, | ||
Prism::MissingNode, | ||
), | ||
).returns(String) | ||
end | ||
def name_with_dynamic_reference(node) | ||
slice = node.slice | ||
slice.gsub(/((?<=::)|^)[a-z]\w*/, DYNAMIC_REFERENCE_MARKER) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "ruby_lsp/listeners/test_style" | ||
|
||
module RubyLsp | ||
module Requests | ||
# This is a custom request to ask the server to parse a test file and discover all available examples in it. Add-ons | ||
# can augment the behavior through listeners, allowing them to handle discovery for different frameworks | ||
class DiscoverTests < Request | ||
extend T::Sig | ||
include Support::Common | ||
|
||
sig { params(global_state: GlobalState, document: RubyDocument, dispatcher: Prism::Dispatcher).void } | ||
def initialize(global_state, document, dispatcher) | ||
super() | ||
@global_state = global_state | ||
@document = document | ||
@dispatcher = dispatcher | ||
@response_builder = T.let(ResponseBuilders::TestItems.new, ResponseBuilders::TestItems) | ||
@index = T.let(global_state.index, RubyIndexer::Index) | ||
end | ||
|
||
sig { override.returns(T::Array[Support::TestItem]) } | ||
def perform | ||
uri = @document.uri | ||
|
||
# We normally only index test files once they are opened in the editor to save memory and avoid doing | ||
# unnecessary work. If the file is already opened and we already indexed it, then we can just discover the tests | ||
# straight away. | ||
# | ||
# However, if the user navigates to a specific test file from the explorer with nothing opened in the UI, then | ||
# we will not have indexed the test file yet and trying to linearize the ancestor of the class will fail. In | ||
# this case, we have to instantiate the indexer listener first, so that we insert classes, modules and methods | ||
# in the index first and then discover the tests, all in the same traversal. | ||
if @index.entries_for(uri.to_s) | ||
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri) | ||
@dispatcher.visit(@document.parse_result.value) | ||
else | ||
@global_state.synchronize do | ||
@index.handle_change(uri) do |index| | ||
RubyIndexer::DeclarationListener.new( | ||
index, | ||
@dispatcher, | ||
@document.parse_result, | ||
uri, | ||
collect_comments: true, | ||
) | ||
|
||
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri) | ||
@dispatcher.dispatch(@document.parse_result.value) | ||
end | ||
end | ||
end | ||
|
||
@response_builder.response | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -103,6 +103,7 @@ unaliased | |
unindexed | ||
unparser | ||
unresolve | ||
vcall | ||
Vinicius | ||
vscodemachineid | ||
vsctm | ||
|
Oops, something went wrong.