@@ -18,6 +18,109 @@ class SorbetLevel < T::Enum
18
18
end
19
19
end
20
20
21
+ class << self
22
+ extend T ::Sig
23
+
24
+ sig do
25
+ params (
26
+ node : Prism ::Node ,
27
+ char_position : Integer ,
28
+ node_types : T ::Array [ T . class_of ( Prism ::Node ) ] ,
29
+ ) . returns ( NodeContext )
30
+ end
31
+ def locate ( node , char_position , node_types : [ ] )
32
+ queue = T . let ( node . child_nodes . compact , T ::Array [ T . nilable ( Prism ::Node ) ] )
33
+ closest = node
34
+ parent = T . let ( nil , T . nilable ( Prism ::Node ) )
35
+ nesting_nodes = T . let (
36
+ [ ] ,
37
+ T ::Array [ T . any (
38
+ Prism ::ClassNode ,
39
+ Prism ::ModuleNode ,
40
+ Prism ::SingletonClassNode ,
41
+ Prism ::DefNode ,
42
+ Prism ::BlockNode ,
43
+ Prism ::LambdaNode ,
44
+ Prism ::ProgramNode ,
45
+ ) ] ,
46
+ )
47
+
48
+ nesting_nodes << node if node . is_a? ( Prism ::ProgramNode )
49
+ call_node = T . let ( nil , T . nilable ( Prism ::CallNode ) )
50
+
51
+ until queue . empty?
52
+ candidate = queue . shift
53
+
54
+ # Skip nil child nodes
55
+ next if candidate . nil?
56
+
57
+ # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
58
+ # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
59
+ # sibling
60
+ T . unsafe ( queue ) . unshift ( *candidate . child_nodes )
61
+
62
+ # Skip if the current node doesn't cover the desired position
63
+ loc = candidate . location
64
+ next unless ( loc . start_offset ...loc . end_offset ) . cover? ( char_position )
65
+
66
+ # If the node's start character is already past the position, then we should've found the closest node
67
+ # already
68
+ break if char_position < loc . start_offset
69
+
70
+ # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
71
+ # and need to pop the stack
72
+ previous_level = nesting_nodes . last
73
+ nesting_nodes . pop if previous_level && loc . start_offset > previous_level . location . end_offset
74
+
75
+ # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
76
+ # the target when it is a constant
77
+ case candidate
78
+ when Prism ::ClassNode , Prism ::ModuleNode , Prism ::SingletonClassNode , Prism ::DefNode , Prism ::BlockNode ,
79
+ Prism ::LambdaNode
80
+ nesting_nodes << candidate
81
+ end
82
+
83
+ if candidate . is_a? ( Prism ::CallNode )
84
+ arg_loc = candidate . arguments &.location
85
+ blk_loc = candidate . block &.location
86
+ if ( arg_loc && ( arg_loc . start_offset ...arg_loc . end_offset ) . cover? ( char_position ) ) ||
87
+ ( blk_loc && ( blk_loc . start_offset ...blk_loc . end_offset ) . cover? ( char_position ) )
88
+ call_node = candidate
89
+ end
90
+ end
91
+
92
+ # If there are node types to filter by, and the current node is not one of those types, then skip it
93
+ next if node_types . any? && node_types . none? { |type | candidate . class == type }
94
+
95
+ # If the current node is narrower than or equal to the previous closest node, then it is more precise
96
+ closest_loc = closest . location
97
+ if loc . end_offset - loc . start_offset <= closest_loc . end_offset - closest_loc . start_offset
98
+ parent = closest
99
+ closest = candidate
100
+ end
101
+ end
102
+
103
+ # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated.
104
+ # That is, when targeting Bar in the following example:
105
+ #
106
+ # ```ruby
107
+ # class Foo::Bar; end
108
+ # ```
109
+ # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack,
110
+ # even though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
111
+ if closest . is_a? ( Prism ::ConstantReadNode ) || closest . is_a? ( Prism ::ConstantPathNode )
112
+ last_level = nesting_nodes . last
113
+
114
+ if ( last_level . is_a? ( Prism ::ModuleNode ) || last_level . is_a? ( Prism ::ClassNode ) ) &&
115
+ last_level . constant_path == closest
116
+ nesting_nodes . pop
117
+ end
118
+ end
119
+
120
+ NodeContext . new ( closest , parent , nesting_nodes , call_node )
121
+ end
122
+ end
123
+
21
124
sig { override . returns ( ParseResultType ) }
22
125
def parse
23
126
return @parse_result unless @needs_parsing
@@ -89,5 +192,15 @@ def locate_first_within_range(range, node_types: [])
89
192
end
90
193
end
91
194
end
195
+
196
+ sig do
197
+ params (
198
+ position : T ::Hash [ Symbol , T . untyped ] ,
199
+ node_types : T ::Array [ T . class_of ( Prism ::Node ) ] ,
200
+ ) . returns ( NodeContext )
201
+ end
202
+ def locate_node ( position , node_types : [ ] )
203
+ RubyDocument . locate ( @parse_result . value , create_scanner . find_char_position ( position ) , node_types : node_types )
204
+ end
92
205
end
93
206
end
0 commit comments