Skip to content

Commit

Permalink
Split constant and method entries
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Apr 24, 2024
1 parent 5338a9a commit 5746b68
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 132 deletions.
2 changes: 1 addition & 1 deletion lib/ruby_indexer/lib/ruby_indexer/collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def handle_private_constant(node)

# The private_constant method does not resolve the constant name. It always points to a constant that needs to
# exist in the current namespace
entries = @index[fully_qualify_name(name)]
entries = @index.get_constant(fully_qualify_name(name))
entries&.each { |entry| entry.visibility = :private }
end

Expand Down
106 changes: 79 additions & 27 deletions lib/ruby_indexer/lib/ruby_indexer/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ class UnresolvableAliasError < StandardError; end

sig { void }
def initialize
# Holds all entries in the index using the following format:
# Holds all constant entries in the index using the following format:
# {
# "Foo" => [#<Entry::Class>, #<Entry::Class>],
# "Foo::Bar" => [#<Entry::Class>],
# }
@entries = T.let({}, T::Hash[String, T::Array[Entry]])
@constant_entries = T.let({}, T::Hash[String, T::Array[Entry]])

# Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
@entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]])
@constant_entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]])

# Holds all method entries in the index using the following format:
# {
# "method_name" => [#<Entry::Method>, #<Entry::Method>],
# "foo" => [#<Entry::Accessor>],
# }
@method_entries = T.let({}, T::Hash[String, T::Array[Entry]])

# Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
@method_entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]])

# Holds references to where entries where discovered so that we can easily delete them
# {
Expand All @@ -38,8 +48,16 @@ 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|
if entry.is_a?(Entry::Member)
entry_set = @method_entries
entry_tree = @method_entries_tree
else
entry_set = @constant_entries
entry_tree = @constant_entries_tree
end

name = entry.name
entries = @entries[name]
entries = entry_set[name]
next unless entries

# Delete the specific entry from the list for this name
Expand All @@ -48,10 +66,10 @@ def delete(indexable)
# If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update
# the prefix tree with the current entries
if entries.empty?
@entries.delete(name)
@entries_tree.delete(name)
entry_set.delete(name)
entry_tree.delete(name)
else
@entries_tree.insert(name, entries)
entry_tree.insert(name, entries)
end
end

Expand All @@ -64,15 +82,28 @@ def delete(indexable)
sig { params(entry: Entry).void }
def <<(entry)
name = entry.name

(@entries[name] ||= []) << entry
(@files_to_entries[entry.file_path] ||= []) << entry
@entries_tree.insert(name, T.must(@entries[name]))

if entry.is_a?(Entry::Member)
entry_set = @method_entries
entry_tree = @method_entries_tree
else
entry_set = @constant_entries
entry_tree = @constant_entries_tree
end

(entry_set[name] ||= []) << entry
entry_tree.insert(name, T.must(entry_set[name]))
end

sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
def [](fully_qualified_name)
@entries[fully_qualified_name.delete_prefix("::")]
def get_constant(fully_qualified_name)
@constant_entries[fully_qualified_name.delete_prefix("::")]
end

sig { params(name: String).returns(T.nilable(T::Array[Entry])) }
def get_method(name)
@method_entries[name]
end

sig { params(query: String).returns(T::Array[IndexablePath]) }
Expand All @@ -94,17 +125,35 @@ def search_require_paths(query)
# ]
# ```
sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) }
def prefix_search(query, nesting = nil)
def prefix_search_constants(query, nesting = nil)
unless nesting
results = @constant_entries_tree.search(query)
results.uniq!
return results
end

results = nesting.length.downto(0).flat_map do |i|
prefix = T.must(nesting[0...i]).join("::")
namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
@constant_entries_tree.search(namespaced_query)
end

results.uniq!
results
end

sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) }
def prefix_search_methods(query, nesting = nil)
unless nesting
results = @entries_tree.search(query)
results = @method_entries_tree.search(query)
results.uniq!
return results
end

results = nesting.length.downto(0).flat_map do |i|
prefix = T.must(nesting[0...i]).join("::")
namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
@entries_tree.search(namespaced_query)
@method_entries_tree.search(namespaced_query)
end

results.uniq!
Expand All @@ -114,11 +163,14 @@ def prefix_search(query, nesting = nil)
# Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
def fuzzy_search(query)
return @entries.flat_map { |_name, entries| entries } unless query
unless query
constants = @constant_entries.flat_map { |_name, entries| entries }
return constants + @method_entries.flat_map { |_name, entries| entries }
end

normalized_query = query.gsub("::", "").downcase

results = @entries.filter_map do |name, entries|
results = @constant_entries.merge(@method_entries).filter_map do |name, entries|
similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
[entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
end
Expand All @@ -132,10 +184,10 @@ def fuzzy_search(query)
# 2. Foo::Baz
# 3. Baz
sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
def resolve(name, nesting)
def resolve_constant(name, nesting)
if name.start_with?("::")
name = name.delete_prefix("::")
results = @entries[name] || @entries[follow_aliased_namespace(name)]
results = @constant_entries[name] || @constant_entries[follow_aliased_namespace(name)]
return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e }
end

Expand All @@ -150,7 +202,7 @@ def resolve(name, nesting)
# 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)]
entries = @constant_entries[full_name] || @constant_entries[follow_aliased_namespace(full_name)]
return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries
end

Expand Down Expand Up @@ -208,14 +260,14 @@ def index_single(indexable_path, source = nil)
# 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)
return name if @entries[name]
return name if @constant_entries[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
entry = @constant_entries[current_name]&.first

case entry
when Entry::Alias
Expand All @@ -242,8 +294,8 @@ def follow_aliased_namespace(name)
# Returns `nil` if the method does not exist on that receiver
sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
def resolve_method(method_name, receiver_name)
method_entries = self[method_name]
owner_entries = self[receiver_name]
method_entries = get_method(method_name)
owner_entries = get_constant(receiver_name)
return unless owner_entries && method_entries

owner_name = T.must(owner_entries.first).name
Expand All @@ -261,18 +313,18 @@ def resolve_method(method_name, receiver_name)
# 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)
target = resolve_constant(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 = T.must(@constant_entries[entry.name])
original_entries.delete(entry)
original_entries << resolved_alias

@entries_tree.insert(entry.name, original_entries)
@constant_entries_tree.insert(entry.name, original_entries)

resolved_alias
end
Expand Down
44 changes: 22 additions & 22 deletions lib/ruby_indexer/test/classes_and_modules_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ class Foo
class Bar; end
RUBY

foo_entry = @index["Foo"].first
foo_entry = @index.get_constant("Foo").first
assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))

bar_entry = @index["Bar"].first
bar_entry = @index.get_constant("Bar").first
assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
end

Expand All @@ -174,7 +174,7 @@ def test_skips_comments_containing_invalid_encodings
class Foo
end
RUBY
assert(@index["Foo"].first)
assert(@index.get_constant("Foo").first)
end

def test_comments_can_be_attached_to_a_namespaced_class
Expand All @@ -187,10 +187,10 @@ class Bar; end
end
RUBY

foo_entry = @index["Foo"].first
foo_entry = @index.get_constant("Foo").first
assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))

bar_entry = @index["Foo::Bar"].first
bar_entry = @index.get_constant("Foo::Bar").first
assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
end

Expand All @@ -203,10 +203,10 @@ class Foo; end
class Foo; end
RUBY

first_foo_entry = @index["Foo"][0]
first_foo_entry = @index.get_constant("Foo")[0]
assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))

second_foo_entry = @index["Foo"][1]
second_foo_entry = @index.get_constant("Foo")[1]
assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
end

Expand All @@ -219,10 +219,10 @@ class Foo; end
class Bar; end
RUBY

first_foo_entry = @index["Foo"][0]
first_foo_entry = @index.get_constant("Foo")[0]
assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))

second_foo_entry = @index["Bar"][0]
second_foo_entry = @index.get_constant("Bar")[0]
assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
end

Expand All @@ -239,13 +239,13 @@ class D; end
end
RUBY

b_const = @index["A::B"].first
b_const = @index.get_constant("A::B").first
assert_equal(:private, b_const.visibility)

c_const = @index["A::C"].first
c_const = @index.get_constant("A::C").first
assert_equal(:private, c_const.visibility)

d_const = @index["A::D"].first
d_const = @index.get_constant("A::D").first
assert_equal(:public, d_const.visibility)
end

Expand All @@ -269,16 +269,16 @@ class FinalThing < Something::Baz
end
RUBY

foo = T.must(@index["Foo"].first)
foo = T.must(@index.get_constant("Foo").first)
assert_equal("Bar", foo.parent_class)

baz = T.must(@index["Baz"].first)
baz = T.must(@index.get_constant("Baz").first)
assert_nil(baz.parent_class)

qux = T.must(@index["Something::Qux"].first)
qux = T.must(@index.get_constant("Something::Qux").first)
assert_equal("::Baz", qux.parent_class)

final_thing = T.must(@index["FinalThing"].first)
final_thing = T.must(@index.get_constant("FinalThing").first)
assert_equal("Something::Baz", final_thing.parent_class)
end

Expand Down Expand Up @@ -318,13 +318,13 @@ class ConstantPathReferences
end
RUBY

foo = T.must(@index["Foo"][0])
foo = T.must(@index.get_constant("Foo")[0])
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)

qux = T.must(@index["Foo::Qux"][0])
qux = T.must(@index.get_constant("Foo::Qux")[0])
assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)

constant_path_references = T.must(@index["ConstantPathReferences"][0])
constant_path_references = T.must(@index.get_constant("ConstantPathReferences")[0])
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
end

Expand Down Expand Up @@ -364,13 +364,13 @@ class ConstantPathReferences
end
RUBY

foo = T.must(@index["Foo"][0])
foo = T.must(@index.get_constant("Foo")[0])
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules)

qux = T.must(@index["Foo::Qux"][0])
qux = T.must(@index.get_constant("Foo::Qux")[0])
assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules)

constant_path_references = T.must(@index["ConstantPathReferences"][0])
constant_path_references = T.must(@index.get_constant("ConstantPathReferences")[0])
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules)
end
end
Expand Down
Loading

0 comments on commit 5746b68

Please sign in to comment.