lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.17.11 vs lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.17.12
- old
+ new
@@ -33,12 +33,33 @@
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
@require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
# Holds the linearized ancestors list for every namespace
@ancestors = T.let({}, T::Hash[String, T::Array[String]])
+
+ # List of classes that are enhancing the index
+ @enhancements = T.let([], T::Array[Enhancement])
+
+ # Map of module name to included hooks that have to be executed when we include the given module
+ @included_hooks = T.let(
+ {},
+ T::Hash[String, T::Array[T.proc.params(index: Index, base: Entry::Namespace).void]],
+ )
end
+ # Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
+ sig { params(enhancement: Enhancement).void }
+ def register_enhancement(enhancement)
+ @enhancements << enhancement
+ end
+
+ # Register an included `hook` that will be executed when `module_name` is included into any namespace
+ sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
+ def register_included_hook(module_name, &hook)
+ (@included_hooks[module_name] ||= []) << hook
+ end
+
sig { params(indexable: IndexablePath).void }
def delete(indexable)
# For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
# left, delete the constant from the index.
@files_to_entries[indexable.full_path]&.each do |entry|
@@ -294,15 +315,29 @@
def index_single(indexable_path, source = nil)
content = source || File.read(indexable_path.full_path)
dispatcher = Prism::Dispatcher.new
result = Prism.parse(content)
- DeclarationListener.new(self, dispatcher, result, indexable_path.full_path)
+ listener = DeclarationListener.new(
+ self,
+ dispatcher,
+ result,
+ indexable_path.full_path,
+ enhancements: @enhancements,
+ )
dispatcher.dispatch(result.value)
+ indexing_errors = listener.indexing_errors.uniq
+
require_path = indexable_path.require_path
@require_paths_tree.insert(require_path, indexable_path) if require_path
+
+ if indexing_errors.any?
+ indexing_errors.each do |error|
+ $stderr.puts error
+ end
+ end
rescue Errno::EISDIR, Errno::ENOENT
# If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
# it
rescue SystemStackError => e
if e.backtrace&.first&.include?("prism")
@@ -455,10 +490,16 @@
singleton_levels.times do
nesting << "<Class:#{T.must(nesting.last)}>"
end
end
+ # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
+ # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
+ # inclusion of another module or the prepending of another module, because those features are already a part of
+ # Ruby and can be used directly without any metaprogramming
+ run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
+
linearize_mixins(ancestors, namespaces, nesting)
linearize_superclass(
ancestors,
attached_class_name,
fully_qualified_name,
@@ -567,9 +608,37 @@
singleton
end
private
+
+ # Runs the registered included hooks
+ sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
+ def run_included_hooks(fully_qualified_name, nesting)
+ return if @included_hooks.empty?
+
+ namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
+ return unless namespaces
+
+ namespaces.each do |namespace|
+ namespace.mixin_operations.each do |operation|
+ next unless operation.is_a?(Entry::Include)
+
+ # First we resolve the include name, so that we know the actual module being referred to in the include
+ resolved_modules = resolve(operation.module_name, nesting)
+ next unless resolved_modules
+
+ module_name = T.must(resolved_modules.first).name
+
+ # Then we grab any hooks registered for that module
+ hooks = @included_hooks[module_name]
+ next unless hooks
+
+ # We invoke the hooks with the index and the namespace that included the module
+ hooks.each { |hook| hook.call(self, namespace) }
+ end
+ end
+ end
# Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
# linearized ancestors of the mixins
sig do
params(