lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.10.1 vs lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.11.0
- old
+ new
@@ -3,10 +3,12 @@
module RubyIndexer
class Index
extend T::Sig
+ class UnresolvableAliasError < StandardError; end
+
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
ENTRY_SIMILARITY_THRESHOLD = 0.7
sig { void }
def initialize
@@ -123,18 +125,34 @@
# 1. Foo::Bar::Baz
# 2. Foo::Baz
# 3. Baz
sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
def resolve(name, nesting)
- (nesting.length + 1).downto(0).each do |i|
- prefix = T.must(nesting[0...i]).join("::")
- full_name = prefix.empty? ? name : "#{prefix}::#{name}"
- entries = @entries[full_name]
- return entries if entries
+ if name.start_with?("::")
+ name = name.delete_prefix("::")
+ results = @entries[name] || @entries[follow_aliased_namespace(name)]
+ return results.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if results
end
+ nesting.length.downto(0).each do |i|
+ namespace = T.must(nesting[0...i]).join("::")
+ full_name = namespace.empty? ? name : "#{namespace}::#{name}"
+
+ # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases -
+ # because the user might be trying to jump to the alias definition.
+ #
+ # However, if we don't find it, then we need to search for possible aliases in the namespace. For example, in
+ # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing
+ # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the
+ # `RubyLsp::Interface` part is an alias, that has to be resolved
+ entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
+ return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
+ end
+
nil
+ rescue UnresolvableAliasError
+ nil
end
sig { params(indexable_paths: T::Array[IndexablePath]).void }
def index_all(indexable_paths: RubyIndexer.configuration.indexables)
indexable_paths.each { |path| index_single(path) }
@@ -150,10 +168,72 @@
@require_paths_tree.insert(require_path, indexable_path) if require_path
rescue Errno::EISDIR
# If `path` is a directory, just ignore it and continue indexing
end
+ # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
+ # it. The idea is that we test the name in parts starting from the complete name to the first namespace. For
+ # `Foo::Bar::Baz`, we would test:
+ # 1. Is `Foo::Bar::Baz` an alias? Get the target and recursively follow its target
+ # 2. Is `Foo::Bar` an alias? Get the target and recursively follow its target
+ # 3. Is `Foo` an alias? Get the target and recursively follow its target
+ #
+ # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to
+ # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other
+ # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name
+ sig { params(name: String).returns(String) }
+ def follow_aliased_namespace(name)
+ parts = name.split("::")
+ real_parts = []
+
+ (parts.length - 1).downto(0).each do |i|
+ current_name = T.must(parts[0..i]).join("::")
+ entry = @entries[current_name]&.first
+
+ case entry
+ when Entry::Alias
+ target = entry.target
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
+ when Entry::UnresolvedAlias
+ resolved = resolve_alias(entry)
+
+ if resolved.is_a?(Entry::UnresolvedAlias)
+ raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
+ end
+
+ target = resolved.target
+ return follow_aliased_namespace("#{target}::#{real_parts.join("::")}")
+ else
+ real_parts.unshift(T.must(parts[i]))
+ end
+ end
+
+ real_parts.join("::")
+ end
+
+ private
+
+ # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
+ # that doesn't exist, then we return the same UnresolvedAlias
+ sig { params(entry: Entry::UnresolvedAlias).returns(T.any(Entry::Alias, Entry::UnresolvedAlias)) }
+ def resolve_alias(entry)
+ target = resolve(entry.target, entry.nesting)
+ return entry unless target
+
+ target_name = T.must(target.first).name
+ resolved_alias = Entry::Alias.new(target_name, entry)
+
+ # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later
+ original_entries = T.must(@entries[entry.name])
+ original_entries.delete(entry)
+ original_entries << resolved_alias
+
+ @entries_tree.insert(entry.name, original_entries)
+
+ resolved_alias
+ end
+
class Entry
extend T::Sig
sig { returns(String) }
attr_reader :name
@@ -165,16 +245,20 @@
attr_reader :location
sig { returns(T::Array[String]) }
attr_reader :comments
+ sig { returns(Symbol) }
+ attr_accessor :visibility
+
sig { params(name: String, file_path: String, location: YARP::Location, comments: T::Array[String]).void }
def initialize(name, file_path, location, comments)
@name = name
@file_path = file_path
@location = location
@comments = comments
+ @visibility = T.let(:public, Symbol)
end
sig { returns(String) }
def file_name
File.basename(@file_path)
@@ -192,9 +276,61 @@
class Class < Namespace
end
class Constant < Entry
+ end
+
+ # An UnresolvedAlias points to a constant alias with a right hand side that has not yet been resolved. For
+ # example, if we find
+ #
+ # ```ruby
+ # CONST = Foo
+ # ```
+ # Before we have discovered `Foo`, there's no way to eagerly resolve this alias to the correct target constant.
+ # All aliases are inserted as UnresolvedAlias in the index first and then we lazily resolve them to the correct
+ # target in [rdoc-ref:Index#resolve]. If the right hand side contains a constant that doesn't exist, then it's not
+ # possible to resolve the alias and it will remain an UnresolvedAlias until the right hand side constant exists
+ class UnresolvedAlias < Entry
+ extend T::Sig
+
+ sig { returns(String) }
+ attr_reader :target
+
+ sig { returns(T::Array[String]) }
+ attr_reader :nesting
+
+ sig do
+ params(
+ target: String,
+ nesting: T::Array[String],
+ name: String,
+ file_path: String,
+ location: YARP::Location,
+ comments: T::Array[String],
+ ).void
+ end
+ def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
+ super(name, file_path, location, comments)
+
+ @target = target
+ @nesting = nesting
+ end
+ end
+
+ # Alias represents a resolved alias, which points to an existing constant target
+ class Alias < Entry
+ extend T::Sig
+
+ sig { returns(String) }
+ attr_reader :target
+
+ sig { params(target: String, unresolved_alias: UnresolvedAlias).void }
+ def initialize(target, unresolved_alias)
+ super(unresolved_alias.name, unresolved_alias.file_path, unresolved_alias.location, unresolved_alias.comments)
+
+ @target = target
+ end
end
end
end
end