From 6148479b57f3253a8b0b546087926d2a0d0b3a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janko=20Marohni=C4=87?= Date: Thu, 11 Dec 2025 21:30:40 +0100 Subject: [PATCH 1/7] Expose `NodeContext#nesting_nodes` attribute reader --- lib/ruby_lsp/node_context.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 1fe2c4631b..70f7f902b8 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -8,6 +8,9 @@ class NodeContext #: Prism::Node? attr_reader :node, :parent + #: Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] + attr_reader :nesting_nodes + #: Array[String] attr_reader :nesting From fcf41c2710a3efa95284987bc0abb180c15ed5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:16:17 -0500 Subject: [PATCH 2/7] Add tests for the nesting_nodes method in NodeContext --- test/ruby_document_test.rb | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/test/ruby_document_test.rb b/test/ruby_document_test.rb index 0b677a43bd..19c031cf1d 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) From 793a0c06e6a4033100e460ac08bd3d01de221e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:19:00 -0500 Subject: [PATCH 3/7] Add type alias for nesting_node in NodeContext class --- lib/ruby_lsp/node_context.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 70f7f902b8..2c95bbac86 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -5,10 +5,12 @@ 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[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] + #: Array[nesting_node] attr_reader :nesting_nodes #: Array[String] @@ -20,7 +22,7 @@ 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 @@ -55,7 +57,7 @@ def locals_for_scope private - #: (Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nodes) -> [Array[String], String?] + #: (Array[nesting_node] nodes) -> [Array[String], String?] def handle_nesting_nodes(nodes) nesting = [] surrounding_method = nil #: String? From 765303d5dc509a34880f8439875c4f4ae39d5042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:19:42 -0500 Subject: [PATCH 4/7] Remove argument that was never used --- lib/ruby_lsp/node_context.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 2c95bbac86..b41c320852 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -29,7 +29,7 @@ def initialize(node, parent, nesting_nodes, call_node) @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 @@ -57,8 +57,8 @@ def locals_for_scope private - #: (Array[nesting_node] nodes) -> [Array[String], String?] - def handle_nesting_nodes(nodes) + #: -> [Array[String], String?] + def handle_nesting_nodes nesting = [] surrounding_method = nil #: String? From cd335b041624ab1cbb982a1f6dbd3a392408e862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:20:41 -0500 Subject: [PATCH 5/7] Use method instead of instance variable for nesting nodes --- lib/ruby_lsp/node_context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index b41c320852..4a9ea79c77 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -43,7 +43,7 @@ def fully_qualified_name def locals_for_scope locals = [] - @nesting_nodes.each do |node| + 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 @@ -62,7 +62,7 @@ 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 From 364cc4a4fe85b26fd1a644f2769fe26970ad5cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:23:25 -0500 Subject: [PATCH 6/7] Extract method to build the singleton class name --- lib/ruby_lsp/node_context.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 4a9ea79c77..3e505a7754 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -67,16 +67,21 @@ def handle_nesting_nodes 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 From d3bce7ab1b267ac307d47677882aa6cf42c3ba6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 19 Jan 2026 17:25:41 -0500 Subject: [PATCH 7/7] Extract a method to better explain what is being done --- lib/ruby_lsp/node_context.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ruby_lsp/node_context.rb b/lib/ruby_lsp/node_context.rb index 3e505a7754..c29f07895a 100644 --- a/lib/ruby_lsp/node_context.rb +++ b/lib/ruby_lsp/node_context.rb @@ -44,11 +44,7 @@ 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 - + locals.clear if scope_boundary?(node) locals.concat(node.locals) end @@ -57,6 +53,12 @@ def locals_for_scope private + #: (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 = []