@@ -35,6 +35,27 @@ def initialize
35
35
36
36
# Holds the linearized ancestors list for every namespace
37
37
@ancestors = T . let ( { } , T ::Hash [ String , T ::Array [ String ] ] )
38
+
39
+ # List of classes that are enhancing the index
40
+ @enhancements = T . let ( [ ] , T ::Array [ Enhancement ] )
41
+
42
+ # Map of module name to included hooks that have to be executed when we include the given module
43
+ @included_hooks = T . let (
44
+ { } ,
45
+ T ::Hash [ String , T ::Array [ T . proc . params ( index : Index , base : Entry ::Namespace ) . void ] ] ,
46
+ )
47
+ end
48
+
49
+ # Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
50
+ sig { params ( enhancement : Enhancement ) . void }
51
+ def register_enhancement ( enhancement )
52
+ @enhancements << enhancement
53
+ end
54
+
55
+ # Register an included `hook` that will be executed when `module_name` is included into any namespace
56
+ sig { params ( module_name : String , hook : T . proc . params ( index : Index , base : Entry ::Namespace ) . void ) . void }
57
+ def register_included_hook ( module_name , &hook )
58
+ ( @included_hooks [ module_name ] ||= [ ] ) << hook
38
59
end
39
60
40
61
sig { params ( indexable : IndexablePath ) . void }
@@ -296,7 +317,7 @@ def index_single(indexable_path, source = nil)
296
317
dispatcher = Prism ::Dispatcher . new
297
318
298
319
result = Prism . parse ( content )
299
- DeclarationListener . new ( self , dispatcher , result , indexable_path . full_path )
320
+ DeclarationListener . new ( self , dispatcher , result , indexable_path . full_path , enhancements : @enhancements )
300
321
dispatcher . dispatch ( result . value )
301
322
302
323
require_path = indexable_path . require_path
@@ -457,6 +478,12 @@ def linearized_ancestors_of(fully_qualified_name)
457
478
end
458
479
end
459
480
481
+ # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
482
+ # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
483
+ # inclusion of another module or the prepending of another module, because those features are already a part of
484
+ # Ruby and can be used directly without any metaprogramming
485
+ run_included_hooks ( attached_class_name , nesting ) if singleton_levels > 0
486
+
460
487
linearize_mixins ( ancestors , namespaces , nesting )
461
488
linearize_superclass (
462
489
ancestors ,
@@ -570,6 +597,34 @@ def existing_or_new_singleton_class(name)
570
597
571
598
private
572
599
600
+ # Runs the registered included hooks
601
+ sig { params ( fully_qualified_name : String , nesting : T ::Array [ String ] ) . void }
602
+ def run_included_hooks ( fully_qualified_name , nesting )
603
+ return if @included_hooks . empty?
604
+
605
+ namespaces = self [ fully_qualified_name ] &.grep ( Entry ::Namespace )
606
+ return unless namespaces
607
+
608
+ namespaces . each do |namespace |
609
+ namespace . mixin_operations . each do |operation |
610
+ next unless operation . is_a? ( Entry ::Include )
611
+
612
+ # First we resolve the include name, so that we know the actual module being referred to in the include
613
+ resolved_modules = resolve ( operation . module_name , nesting )
614
+ next unless resolved_modules
615
+
616
+ module_name = T . must ( resolved_modules . first ) . name
617
+
618
+ # Then we grab any hooks registered for that module
619
+ hooks = @included_hooks [ module_name ]
620
+ next unless hooks
621
+
622
+ # We invoke the hooks with the index and the namespace that included the module
623
+ hooks . each { |hook | hook . call ( self , namespace ) }
624
+ end
625
+ end
626
+ end
627
+
573
628
# Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
574
629
# linearized ancestors of the mixins
575
630
sig do
0 commit comments