class RubyLsp::Listeners::SpecStyle
def extract_description(node)
def extract_description(node) first_argument = node.arguments&.arguments&.first return unless first_argument case first_argument when Prism::StringNode first_argument.content when Prism::ConstantReadNode, Prism::ConstantPathNode constant_name(first_argument) else first_argument.slice end end
def handle_describe(node)
def handle_describe(node) # Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes # that ignore describes return if node.block.nil? description = extract_description(node) return unless description parent = latest_group return unless parent id = case parent when Requests::Support::TestItem "#{parent.id}::#{description}" else description end test_item = Requests::Support::TestItem.new( id, description, @uri, range_from_node(node), framework: :minitest, ) parent.add(test_item) @response_builder.add_code_lens(test_item) @spec_group_id_stack << DescribeGroup.new(id) end
def handle_example(node)
def handle_example(node) # Minitest formats the descriptions into test method names by using the count of examples with the description # We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number # instead. Note that anonymous examples mixed with meta-programming will not be handled correctly description = extract_description(node) || "anonymous" line = node.location.start_line - 1 parent = latest_group return unless parent.is_a?(Requests::Support::TestItem) id = "#{parent.id}##{format("test_%04d_%s", line, description)}" test_item = Requests::Support::TestItem.new( id, description, @uri, range_from_node(node), framework: :minitest, ) parent.add(test_item) @response_builder.add_code_lens(test_item) end
def in_spec_context?
def in_spec_context? @nesting.empty? || @spec_group_id_stack.any? { |id| id } end
def initialize(response_builder, global_state, dispatcher, uri)
def initialize(response_builder, global_state, dispatcher, uri) super(response_builder, global_state, uri) @spec_group_id_stack = [] #: Array[Group?] register_events( dispatcher, :on_class_node_enter, :on_call_node_enter, :on_call_node_leave, ) end
def latest_group
def latest_group # If we haven't found anything yet, then return the response builder return @response_builder if @spec_group_id_stack.compact.empty? # If we found something that isn't a group last, then we're inside a random module or class, but not a spec # group return unless @spec_group_id_stack.last # Specs using at least one class as a group require special handling closest_class_index = @spec_group_id_stack.rindex { |i| i.is_a?(ClassGroup) } if closest_class_index first_class_index = @spec_group_id_stack.index { |i| i.is_a?(ClassGroup) } #: as !nil first_class = @spec_group_id_stack[first_class_index] #: as !nil item = @response_builder[first_class.id] #: as !nil # Descend into child items from the beginning all the way to the latest class group, ignoring describes @spec_group_id_stack[first_class_index + 1..closest_class_index] #: as !nil .each do |group| next unless group.is_a?(ClassGroup) item = item[group.id] #: as !nil end # From the class forward, we must take describes into account @spec_group_id_stack[closest_class_index + 1..] #: as !nil .each do |group| next unless group item = item[group.id] #: as !nil end return item end # Specs only using describes first_group = @spec_group_id_stack.find { |i| i.is_a?(DescribeGroup) } return unless first_group item = @response_builder[first_group.id] #: as !nil @spec_group_id_stack[1..] #: as !nil .each do |group| next unless group.is_a?(DescribeGroup) item = item[group.id] #: as !nil end item end
def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::CallNode) -> void
def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod return unless in_spec_context? case node.name when :describe handle_describe(node) when :it, :specify handle_example(node) end end
def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::CallNode) -> void
def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod return unless node.name == :describe && !node.receiver current_group = @spec_group_id_stack.last return unless current_group.is_a?(DescribeGroup) description = extract_description(node) return unless description && current_group.id.end_with?(description) @spec_group_id_stack.pop end
def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::ClassNode) -> void
def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod with_test_ancestor_tracking(node) do |name, ancestors| @spec_group_id_stack << (ancestors.include?("Minitest::Spec") ? ClassGroup.new(name) : nil) end end
def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::ClassNode) -> void
def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod @spec_group_id_stack.pop super end
def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::ModuleNode) -> void
def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod @spec_group_id_stack << nil super end
def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
: (Prism::ModuleNode) -> void
def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod @spec_group_id_stack.pop super end