diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 1fe2c4631..c29f07895 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -5,9 +5,14 @@ module RubyLsp # This class allows listeners to access contextual information about a node in the AST, such as its parent, # its namespace nesting, and the surrounding CallNode (e.g. a method call). class NodeContext + #: type nesting_node = Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode + #: Prism::Node? attr_reader :node, :parent + #: Array[nesting_node] + attr_reader :nesting_nodes + #: Array[String] attr_reader :nesting @@ -17,14 +22,14 @@ class NodeContext #: String? attr_reader :surrounding_method - #: (Prism::Node? node, Prism::Node? parent, Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nesting_nodes, Prism::CallNode? call_node) -> void + #: (Prism::Node? node, Prism::Node? parent, Array[nesting_node] nesting_nodes, Prism::CallNode? call_node) -> void def initialize(node, parent, nesting_nodes, call_node) @node = node @parent = parent @nesting_nodes = nesting_nodes @call_node = call_node - nesting, surrounding_method = handle_nesting_nodes(nesting_nodes) + nesting, surrounding_method = handle_nesting_nodes @nesting = nesting #: Array[String] @surrounding_method = surrounding_method #: String? end @@ -38,12 +43,8 @@ def fully_qualified_name def locals_for_scope locals = [] - @nesting_nodes.each do |node| - if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) || node.is_a?(Prism::SingletonClassNode) || - node.is_a?(Prism::DefNode) - locals.clear - end - + nesting_nodes.each do |node| + locals.clear if scope_boundary?(node) locals.concat(node.locals) end @@ -52,26 +53,37 @@ def locals_for_scope private - #: (Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nodes) -> [Array[String], String?] - def handle_nesting_nodes(nodes) + #: (nesting_node node) -> bool + def scope_boundary?(node) + node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) || + node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) + end + + #: -> [Array[String], String?] + def handle_nesting_nodes nesting = [] surrounding_method = nil #: String? - @nesting_nodes.each do |node| + nesting_nodes.each do |node| case node when Prism::ClassNode, Prism::ModuleNode nesting << node.constant_path.slice when Prism::SingletonClassNode - nesting << "" + nesting << singleton_class_name(nesting) when Prism::DefNode surrounding_method = node.name.to_s next unless node.receiver.is_a?(Prism::SelfNode) - nesting << "" + nesting << singleton_class_name(nesting) end end [nesting, surrounding_method] end + + #: (Array[String] nesting) -> String + def singleton_class_name(nesting) + "" + end end end diff --git a/test/ruby_document_test.rb b/test/ruby_document_test.rb index 0b677a43b..19c031cf1 100644 --- a/test/ruby_document_test.rb +++ b/test/ruby_document_test.rb @@ -1078,6 +1078,109 @@ class Foo assert_predicate(document, :should_index?) end + def test_locate_returns_nesting_nodes_for_class_and_module + document = RubyLsp::RubyDocument.new(source: <<~RUBY, version: 1, uri: @uri, global_state: @global_state) + module Foo + class Bar + def baz + hello + end + end + end + RUBY + + node_context = document.locate_node({ line: 3, character: 6 }) + nesting_nodes = node_context.nesting_nodes + + assert_equal(4, nesting_nodes.length) + assert_instance_of(Prism::ProgramNode, nesting_nodes[0]) + assert_instance_of(Prism::ModuleNode, nesting_nodes[1]) + assert_instance_of(Prism::ClassNode, nesting_nodes[2]) + assert_instance_of(Prism::DefNode, nesting_nodes[3]) + + module_node = nesting_nodes[1] #: as Prism::ModuleNode + assert_equal("Foo", module_node.constant_path.slice) + + class_node = nesting_nodes[2] #: as Prism::ClassNode + assert_equal("Bar", class_node.constant_path.slice) + + def_node = nesting_nodes[3] #: as Prism::DefNode + assert_equal(:baz, def_node.name) + end + + def test_locate_returns_nesting_nodes_for_singleton_class + document = RubyLsp::RubyDocument.new(source: <<~RUBY, version: 1, uri: @uri, global_state: @global_state) + class Foo + class << self + def bar + hello + end + end + end + RUBY + + node_context = document.locate_node({ line: 3, character: 6 }) + nesting_nodes = node_context.nesting_nodes + + assert_equal(4, nesting_nodes.length) + assert_instance_of(Prism::ProgramNode, nesting_nodes[0]) + assert_instance_of(Prism::ClassNode, nesting_nodes[1]) + assert_instance_of(Prism::SingletonClassNode, nesting_nodes[2]) + assert_instance_of(Prism::DefNode, nesting_nodes[3]) + end + + def test_locate_returns_nesting_nodes_for_blocks + document = RubyLsp::RubyDocument.new(source: <<~RUBY, version: 1, uri: @uri, global_state: @global_state) + class Foo + def bar + items.each do |item| + process(item) + end + end + end + RUBY + + node_context = document.locate_node({ line: 3, character: 6 }) + nesting_nodes = node_context.nesting_nodes + + assert_equal(4, nesting_nodes.length) + assert_instance_of(Prism::ProgramNode, nesting_nodes[0]) + assert_instance_of(Prism::ClassNode, nesting_nodes[1]) + assert_instance_of(Prism::DefNode, nesting_nodes[2]) + assert_instance_of(Prism::BlockNode, nesting_nodes[3]) + end + + def test_locate_returns_nesting_nodes_for_lambdas + document = RubyLsp::RubyDocument.new(source: <<~RUBY, version: 1, uri: @uri, global_state: @global_state) + class Foo + def bar + processor = ->(item) { process(item) } + end + end + RUBY + + node_context = document.locate_node({ line: 2, character: 37 }) + nesting_nodes = node_context.nesting_nodes + + assert_equal(4, nesting_nodes.length) + assert_instance_of(Prism::ProgramNode, nesting_nodes[0]) + assert_instance_of(Prism::ClassNode, nesting_nodes[1]) + assert_instance_of(Prism::DefNode, nesting_nodes[2]) + assert_instance_of(Prism::LambdaNode, nesting_nodes[3]) + end + + def test_nesting_nodes_at_top_level + document = RubyLsp::RubyDocument.new(source: <<~RUBY, version: 1, uri: @uri, global_state: @global_state) + puts "Hello, World!" + RUBY + + node_context = document.locate_node({ line: 0, character: 5 }) + nesting_nodes = node_context.nesting_nodes + + assert_equal(1, nesting_nodes.length) + assert_instance_of(Prism::ProgramNode, nesting_nodes[0]) + end + private def assert_error_edit(actual, error_range)