Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions lib/ruby_lsp/node_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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 << "<Class:#{nesting.flat_map { |n| n.split("::") }.last}>"
nesting << singleton_class_name(nesting)
when Prism::DefNode
surrounding_method = node.name.to_s
next unless node.receiver.is_a?(Prism::SelfNode)

nesting << "<Class:#{nesting.flat_map { |n| n.split("::") }.last}>"
nesting << singleton_class_name(nesting)
end
end

[nesting, surrounding_method]
end

#: (Array[String] nesting) -> String
def singleton_class_name(nesting)
"<Class:#{nesting.flat_map { |n| n.split("::") }.last}>"
end
end
end
103 changes: 103 additions & 0 deletions test/ruby_document_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading