Repository: Shopify/ruby-lsp (Ruby)

Issue:
Stack overflow when resolving self-referential constant alias
### Description

When a module defines a constant alias that references another constant of the same name in an outer namespace (for example, `B = B::C` inside `A::D`),  the Ruby LSP indexer enters infinite recursion while resolving the alias.  

This leads to a `SystemStackError` ("stack level too deep") during constant resolution.

### Ruby LSP Information

Following code is going to cause SystemStackError exception during reference search:

```ruby
  module A
    module B
      class C
      end
    end
  end

  module A
    module D
      B = B::C
    end
  end
```

Example log:
```
#<Thread:0x0000000125355e00 /Users/pkondzior/Projects/ruby-lsp/lib/ruby_lsp/base_server.rb:155 run> terminated with exception (report_on_exception is true):
/Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:427:in 'Integer#downto': stack level too deep (SystemStackError)
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:427:in 'RubyIndexer::Index#follow_aliased_namespace'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:1034:in 'RubyIndexer::Index#direct_or_aliased_constant'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:332:in 'RubyIndexer::Index#resolve'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:910:in 'RubyIndexer::Index#resolve_alias'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:436:in 'block in RubyIndexer::Index#follow_aliased_namespace'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:427:in 'Integer#downto'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:427:in 'RubyIndexer::Index#follow_aliased_namespace'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_indexer/lib/ruby_indexer/index.rb:1034:in 'RubyIndexer::Index#direct_or_aliased_constant'
	 ... 8542 levels...
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_lsp/requests/references.rb:63:in 'RubyLsp::Requests::References#perform'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_lsp/server.rb:783:in 'RubyLsp::Server#text_document_references'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_lsp/server.rb:78:in 'RubyLsp::Server#process_message'
	from /Users/pkondzior/Projects/ruby-lsp/lib/ruby_lsp/base_server.rb:167:in 'block in RubyLsp::BaseServer#new_worker'
```


### Reproduction steps
1. Run this code in irb console where ruby-lsp is available
```ruby
require "uri"
require "ruby_lsp/internal"

index = RubyIndexer::Index.new

src = <<~RUBY
  module A
    module B
      class C; end
    end
  end

  module A
    module D
      B = B::C
    end
  end
RUBY

uri = URI.parse("file:///test.rb")
index.index_single(uri, src)

# Triggers stack overflow
index.resolve("A::D::B", [])
```





Discussion:

Patch:
diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb
index 7557c9fc25..bc36927534 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/index.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb
@@ -424,7 +424,10 @@ def follow_aliased_namespace(name, seen_names = [])
       (parts.length - 1).downto(0) do |i|
         current_name = parts[0..i] #: as !nil
           .join("::")
-        entry = @entries[current_name]&.first
+
+        entry = unless seen_names.include?(current_name)
+          @entries[current_name]&.first
+        end
 
         case entry
         when Entry::ConstantAlias
@@ -1023,11 +1026,19 @@ def build_non_redundant_full_name(name, nesting)
       name_parts.join("::")
     end
 
+    # Tries to return direct entry from index then non seen canonicalized alias or nil
     #: (String full_name, Array[String] seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
     def direct_or_aliased_constant(full_name, seen_names)
-      entries = @entries[full_name] || @entries[follow_aliased_namespace(full_name)]
+      if (entries = @entries[full_name])
+        return entries.map do |e|
+          e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
+        end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
+      end
+
+      aliased = follow_aliased_namespace(full_name, seen_names)
+      return if full_name == aliased || seen_names.include?(aliased)
 
-      entries&.map do |e|
+      @entries[aliased]&.map do |e|
         e.is_a?(Entry::UnresolvedConstantAlias) ? resolve_alias(e, seen_names) : e
       end #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias])?
     end


Test patch:
diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb
index 688f610522..5c39ef3b9f 100644
--- a/lib/ruby_indexer/test/index_test.rb
+++ b/lib/ruby_indexer/test/index_test.rb
@@ -1152,6 +1152,29 @@ class Foo
       assert_equal(2, foo_entry.location.start_line)
     end
 
+    def test_resolving_self_referential_constant_alias
+      index(<<~RUBY)
+        module A
+          module B
+            class C
+            end
+          end
+        end
+
+        module A
+          module D
+            B = B::C
+          end
+        end
+      RUBY
+
+      entry = @index.resolve("A::D::B", [])&.first #: as Entry::ConstantAlias
+
+      assert_kind_of(RubyIndexer::Entry::ConstantAlias, entry)
+      assert_equal(10, entry.location.start_line)
+      assert_equal("A::B::C", entry.target)
+    end
+
     def test_resolving_qualified_references
       index(<<~RUBY)
         module Namespace