Skip to content

Commit f8cdbfa

Browse files
authored
Allow indexing enhancements to create namespaces (#2857)
### Motivation We realized that in order to support concerns `class_methods do...end`, indexing enhancements needed to be more powerful and have the liberty to create fake/temporary namespaces. When trying to make that possible, we also realized that the API had many shortcomings that made enhancements harder than they had to be. ### Implementation The idea is to instantiate enhancements with the declaration listener and then expose the API from it. That way, the declaration listener can hold important state like the code units cache and the current file path and enhancements only make flow adjustments and additions to the indexing process. This PR also allows enhancements to create modules and classes, which allows us to support `class_methods do...end` in the Rails add-on and allows the RSpec add-on to support `let` and `subject` properly. ### Automated Tests Added tests.
1 parent a738d5b commit f8cdbfa

File tree

5 files changed

+288
-144
lines changed

5 files changed

+288
-144
lines changed

jekyll/add-ons.markdown

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,11 @@ This is how you could write an enhancement to teach the Ruby LSP to understand t
271271
class MyIndexingEnhancement < RubyIndexer::Enhancement
272272
# This on call node handler is invoked any time during indexing when we find a method call. It can be used to insert
273273
# more entries into the index depending on the conditions
274-
def on_call_node_enter(owner, node, file_path, code_units_cache)
275-
return unless owner
274+
def on_call_node_enter(node)
275+
return unless @listener.current_owner
276276

277-
# Get the ancestors of the current class
278-
ancestors = @index.linearized_ancestors_of(owner.name)
279-
280-
# Return early unless the method call is the one we want to handle and the class invoking the DSL inherits from
281-
# our library's parent class
282-
return unless node.name == :my_dsl_that_creates_methods && ancestors.include?("MyLibrary::ParentClass")
277+
# Return early unless the method call is the one we want to handle
278+
return unless node.name == :my_dsl_that_creates_methods
283279

284280
# Create a new entry to be inserted in the index. This entry will represent the declaration that is created via
285281
# meta-programming. All entries are defined in the `entry.rb` file.
@@ -293,24 +289,16 @@ class MyIndexingEnhancement < RubyIndexer::Enhancement
293289
RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: :a)])
294290
]
295291

296-
new_entry = RubyIndexer::Entry::Method.new(
297-
"new_method", # The name of the method that gets created via meta-programming
298-
file_path, # The file_path where the DSL call was found. This should always just be the file_path received
299-
location, # The Prism node location where the DSL call was found
300-
location, # The Prism node location for the DSL name location. May or not be the same
301-
nil, # The documentation for this DSL call. This should always be `nil` to ensure lazy fetching of docs
302-
signatures, # All signatures for this method (every way it can be invoked)
303-
RubyIndexer::Entry::Visibility::PUBLIC, # The method's visibility
304-
owner, # The method's owner. This is almost always going to be the same owner received
292+
@listener.add_method(
293+
"new_method", # Name of the method
294+
location, # Prism location for the node defining this method
295+
signatures # Signatures available to invoke this method
305296
)
306-
307-
# Push the new entry to the index
308-
@index.add(new_entry)
309297
end
310298

311299
# This method is invoked when the parser has finished processing the method call node.
312300
# It can be used to perform cleanups like popping a stack...etc.
313-
def on_call_node_leave(owner, node, file_path, code_units_cache); end
301+
def on_call_node_leave(node); end
314302
end
315303
```
316304

lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb

Lines changed: 114 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ class DeclarationListener
1818
parse_result: Prism::ParseResult,
1919
file_path: String,
2020
collect_comments: T::Boolean,
21-
enhancements: T::Array[Enhancement],
2221
).void
2322
end
24-
def initialize(index, dispatcher, parse_result, file_path, collect_comments: false, enhancements: [])
23+
def initialize(index, dispatcher, parse_result, file_path, collect_comments: false)
2524
@index = index
2625
@file_path = file_path
27-
@enhancements = enhancements
26+
@enhancements = T.let(Enhancement.all(self), T::Array[Enhancement])
2827
@visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
2928
@comments_by_line = T.let(
3029
parse_result.comments.to_h do |c|
@@ -86,15 +85,9 @@ def initialize(index, dispatcher, parse_result, file_path, collect_comments: fal
8685

8786
sig { params(node: Prism::ClassNode).void }
8887
def on_class_node_enter(node)
89-
@visibility_stack.push(Entry::Visibility::PUBLIC)
9088
constant_path = node.constant_path
91-
name = constant_path.slice
92-
93-
comments = collect_comments(node)
94-
9589
superclass = node.superclass
96-
97-
nesting = actual_nesting(name)
90+
nesting = actual_nesting(constant_path.slice)
9891

9992
parent_class = case superclass
10093
when Prism::ConstantReadNode, Prism::ConstantPathNode
@@ -113,53 +106,29 @@ def on_class_node_enter(node)
113106
end
114107
end
115108

116-
entry = Entry::Class.new(
109+
add_class(
117110
nesting,
118-
@file_path,
119-
Location.from_prism_location(node.location, @code_units_cache),
120-
Location.from_prism_location(constant_path.location, @code_units_cache),
121-
comments,
122-
parent_class,
111+
node.location,
112+
constant_path.location,
113+
parent_class_name: parent_class,
114+
comments: collect_comments(node),
123115
)
124-
125-
@owner_stack << entry
126-
@index.add(entry)
127-
@stack << name
128116
end
129117

130118
sig { params(node: Prism::ClassNode).void }
131119
def on_class_node_leave(node)
132-
@stack.pop
133-
@owner_stack.pop
134-
@visibility_stack.pop
120+
pop_namespace_stack
135121
end
136122

137123
sig { params(node: Prism::ModuleNode).void }
138124
def on_module_node_enter(node)
139-
@visibility_stack.push(Entry::Visibility::PUBLIC)
140125
constant_path = node.constant_path
141-
name = constant_path.slice
142-
143-
comments = collect_comments(node)
144-
145-
entry = Entry::Module.new(
146-
actual_nesting(name),
147-
@file_path,
148-
Location.from_prism_location(node.location, @code_units_cache),
149-
Location.from_prism_location(constant_path.location, @code_units_cache),
150-
comments,
151-
)
152-
153-
@owner_stack << entry
154-
@index.add(entry)
155-
@stack << name
126+
add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node))
156127
end
157128

158129
sig { params(node: Prism::ModuleNode).void }
159130
def on_module_node_leave(node)
160-
@stack.pop
161-
@owner_stack.pop
162-
@visibility_stack.pop
131+
pop_namespace_stack
163132
end
164133

165134
sig { params(node: Prism::SingletonClassNode).void }
@@ -201,9 +170,7 @@ def on_singleton_class_node_enter(node)
201170

202171
sig { params(node: Prism::SingletonClassNode).void }
203172
def on_singleton_class_node_leave(node)
204-
@stack.pop
205-
@owner_stack.pop
206-
@visibility_stack.pop
173+
pop_namespace_stack
207174
end
208175

209176
sig { params(node: Prism::MultiWriteNode).void }
@@ -318,7 +285,7 @@ def on_call_node_enter(node)
318285
end
319286

320287
@enhancements.each do |enhancement|
321-
enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
288+
enhancement.on_call_node_enter(node)
322289
rescue StandardError => e
323290
@indexing_errors << <<~MSG
324291
Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
@@ -339,7 +306,7 @@ def on_call_node_leave(node)
339306
end
340307

341308
@enhancements.each do |enhancement|
342-
enhancement.on_call_node_leave(@owner_stack.last, node, @file_path, @code_units_cache)
309+
enhancement.on_call_node_leave(node)
343310
rescue StandardError => e
344311
@indexing_errors << <<~MSG
345312
Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
@@ -464,6 +431,98 @@ def on_alias_method_node_enter(node)
464431
)
465432
end
466433

434+
sig do
435+
params(
436+
name: String,
437+
node_location: Prism::Location,
438+
signatures: T::Array[Entry::Signature],
439+
visibility: Entry::Visibility,
440+
comments: T.nilable(String),
441+
).void
442+
end
443+
def add_method(name, node_location, signatures, visibility: Entry::Visibility::PUBLIC, comments: nil)
444+
location = Location.from_prism_location(node_location, @code_units_cache)
445+
446+
@index.add(Entry::Method.new(
447+
name,
448+
@file_path,
449+
location,
450+
location,
451+
comments,
452+
signatures,
453+
visibility,
454+
@owner_stack.last,
455+
))
456+
end
457+
458+
sig do
459+
params(
460+
name: String,
461+
full_location: Prism::Location,
462+
name_location: Prism::Location,
463+
comments: T.nilable(String),
464+
).void
465+
end
466+
def add_module(name, full_location, name_location, comments: nil)
467+
location = Location.from_prism_location(full_location, @code_units_cache)
468+
name_loc = Location.from_prism_location(name_location, @code_units_cache)
469+
470+
entry = Entry::Module.new(
471+
actual_nesting(name),
472+
@file_path,
473+
location,
474+
name_loc,
475+
comments,
476+
)
477+
478+
advance_namespace_stack(name, entry)
479+
end
480+
481+
sig do
482+
params(
483+
name_or_nesting: T.any(String, T::Array[String]),
484+
full_location: Prism::Location,
485+
name_location: Prism::Location,
486+
parent_class_name: T.nilable(String),
487+
comments: T.nilable(String),
488+
).void
489+
end
490+
def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
491+
nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : actual_nesting(name_or_nesting)
492+
entry = Entry::Class.new(
493+
nesting,
494+
@file_path,
495+
Location.from_prism_location(full_location, @code_units_cache),
496+
Location.from_prism_location(name_location, @code_units_cache),
497+
comments,
498+
parent_class_name,
499+
)
500+
501+
advance_namespace_stack(T.must(nesting.last), entry)
502+
end
503+
504+
sig { params(block: T.proc.params(index: Index, base: Entry::Namespace).void).void }
505+
def register_included_hook(&block)
506+
owner = @owner_stack.last
507+
return unless owner
508+
509+
@index.register_included_hook(owner.name) do |index, base|
510+
block.call(index, base)
511+
end
512+
end
513+
514+
sig { void }
515+
def pop_namespace_stack
516+
@stack.pop
517+
@owner_stack.pop
518+
@visibility_stack.pop
519+
end
520+
521+
sig { returns(T.nilable(Entry::Namespace)) }
522+
def current_owner
523+
@owner_stack.last
524+
end
525+
467526
private
468527

469528
sig do
@@ -921,5 +980,13 @@ def actual_nesting(name)
921980

922981
corrected_nesting
923982
end
983+
984+
sig { params(short_name: String, entry: Entry::Namespace).void }
985+
def advance_namespace_stack(short_name, entry)
986+
@visibility_stack.push(Entry::Visibility::PUBLIC)
987+
@owner_stack << entry
988+
@index.add(entry)
989+
@stack << short_name
990+
end
924991
end
925992
end

lib/ruby_indexer/lib/ruby_indexer/enhancement.rb

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,41 @@ class Enhancement
88

99
abstract!
1010

11-
sig { params(index: Index).void }
12-
def initialize(index)
13-
@index = index
11+
@enhancements = T.let([], T::Array[T::Class[Enhancement]])
12+
13+
class << self
14+
extend T::Sig
15+
16+
sig { params(child: T::Class[Enhancement]).void }
17+
def inherited(child)
18+
@enhancements << child
19+
super
20+
end
21+
22+
sig { params(listener: DeclarationListener).returns(T::Array[Enhancement]) }
23+
def all(listener)
24+
@enhancements.map { |enhancement| enhancement.new(listener) }
25+
end
26+
27+
# Only available for testing purposes
28+
sig { void }
29+
def clear
30+
@enhancements.clear
31+
end
32+
end
33+
34+
sig { params(listener: DeclarationListener).void }
35+
def initialize(listener)
36+
@listener = listener
1437
end
1538

1639
# The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
1740
# register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
1841
# `ClassMethods` modules
19-
sig do
20-
overridable.params(
21-
owner: T.nilable(Entry::Namespace),
22-
node: Prism::CallNode,
23-
file_path: String,
24-
code_units_cache: T.any(
25-
T.proc.params(arg0: Integer).returns(Integer),
26-
Prism::CodeUnitsCache,
27-
),
28-
).void
29-
end
30-
def on_call_node_enter(owner, node, file_path, code_units_cache); end
31-
32-
sig do
33-
overridable.params(
34-
owner: T.nilable(Entry::Namespace),
35-
node: Prism::CallNode,
36-
file_path: String,
37-
code_units_cache: T.any(
38-
T.proc.params(arg0: Integer).returns(Integer),
39-
Prism::CodeUnitsCache,
40-
),
41-
).void
42-
end
43-
def on_call_node_leave(owner, node, file_path, code_units_cache); end
42+
sig { overridable.params(node: Prism::CallNode).void }
43+
def on_call_node_enter(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
44+
45+
sig { overridable.params(node: Prism::CallNode).void }
46+
def on_call_node_leave(node); end # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
4447
end
4548
end

lib/ruby_indexer/lib/ruby_indexer/index.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ def initialize
4040
# Holds the linearized ancestors list for every namespace
4141
@ancestors = T.let({}, T::Hash[String, T::Array[String]])
4242

43-
# List of classes that are enhancing the index
44-
@enhancements = T.let([], T::Array[Enhancement])
45-
4643
# Map of module name to included hooks that have to be executed when we include the given module
4744
@included_hooks = T.let(
4845
{},
@@ -52,12 +49,6 @@ def initialize
5249
@configuration = T.let(RubyIndexer::Configuration.new, Configuration)
5350
end
5451

55-
# Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
56-
sig { params(enhancement: Enhancement).void }
57-
def register_enhancement(enhancement)
58-
@enhancements << enhancement
59-
end
60-
6152
# Register an included `hook` that will be executed when `module_name` is included into any namespace
6253
sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
6354
def register_included_hook(module_name, &hook)
@@ -396,7 +387,6 @@ def index_single(indexable_path, source = nil, collect_comments: true)
396387
result,
397388
indexable_path.full_path,
398389
collect_comments: collect_comments,
399-
enhancements: @enhancements,
400390
)
401391
dispatcher.dispatch(result.value)
402392

0 commit comments

Comments
 (0)