Skip to content

Commit

Permalink
Protect method resolution from circular aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Aug 28, 2024
1 parent 1891baa commit 9e3da2a
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 8 deletions.
23 changes: 15 additions & 8 deletions lib/ruby_indexer/lib/ruby_indexer/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def method_completion_candidates(name, receiver_name)
next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)

if entry.is_a?(Entry::UnresolvedMethodAlias)
resolved_alias = resolve_method_alias(entry, receiver_name)
resolved_alias = resolve_method_alias(entry, receiver_name, [])
hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
else
hash[entry_name] = [entry, ancestor_index]
Expand Down Expand Up @@ -394,16 +394,18 @@ def follow_aliased_namespace(name, seen_names = [])
real_parts.join("::")
end

# Attempts to find methods for a resolved fully qualified receiver name.
# Attempts to find methods for a resolved fully qualified receiver name. Do not provide the `seen_names` parameter
# as it is used only internally to prevent infinite loops when resolving circular aliases
# Returns `nil` if the method does not exist on that receiver
sig do
params(
method_name: String,
receiver_name: String,
seen_names: T::Array[String],
inherited_only: T::Boolean,
).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
end
def resolve_method(method_name, receiver_name, inherited_only: false)
def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false)
method_entries = self[method_name]
return unless method_entries

Expand All @@ -418,7 +420,7 @@ def resolve_method(method_name, receiver_name, inherited_only: false)
when Entry::UnresolvedMethodAlias
# Resolve aliases lazily as we find them
if entry.owner&.name == ancestor
resolved_alias = resolve_method_alias(entry, receiver_name)
resolved_alias = resolve_method_alias(entry, receiver_name, seen_names)
resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
end
end
Expand Down Expand Up @@ -919,16 +921,21 @@ def direct_or_aliased_constant(full_name, seen_names)
params(
entry: Entry::UnresolvedMethodAlias,
receiver_name: String,
seen_names: T::Array[String],
).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias))
end
def resolve_method_alias(entry, receiver_name)
return entry if entry.new_name == entry.old_name
def resolve_method_alias(entry, receiver_name, seen_names)
new_name = entry.new_name
return entry if new_name == entry.old_name
return entry if seen_names.include?(new_name)

seen_names << new_name

target_method_entries = resolve_method(entry.old_name, receiver_name)
target_method_entries = resolve_method(entry.old_name, receiver_name, seen_names)
return entry unless target_method_entries

resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry)
original_entries = T.must(@entries[entry.new_name])
original_entries = T.must(@entries[new_name])
original_entries.delete(entry)
original_entries << resolved_alias
resolved_alias
Expand Down
20 changes: 20 additions & 0 deletions lib/ruby_indexer/test/index_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1822,5 +1822,25 @@ class Child < Namespace::Parent
@index.linearized_ancestors_of("Foo::Child::<Class:Child>"),
)
end

def test_resolving_circular_method_aliases_on_class_reopen
index(<<~RUBY)
class Foo
alias bar ==
def ==(other) = true
end
class Foo
alias == bar
end
RUBY

method = @index.resolve_method("==", "Foo").first
assert_kind_of(Entry::Method, method)
assert_equal("==", method.name)

candidates = @index.method_completion_candidates("=", "Foo")
assert_equal(["==", "==="], candidates.map(&:name))
end
end
end

0 comments on commit 9e3da2a

Please sign in to comment.