Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle constant aliases and ability to follow them #1023

Merged
merged 1 commit into from
Sep 25, 2023

Conversation

vinistock
Copy link
Member

@vinistock vinistock commented Sep 18, 2023

Motivation

Closes #916

First step for #908

We need to handle constant aliases properly, so that we can provide go to definition, completion and hover for them.

I'd appreciate feedback on the proposed API here.

Implementation

The idea of the implementation is the following:

  • When we find a constant alias, there's a chance we haven't indexed the target constant yet, so we can't eagerly try to resolve it
  • Instead, we place UnresolvedAlias entries in the index, which remember the target name and the nesting where the alias was found
  • Then, I'm proposing the addition of follow_alias, which does two things:
    • Turns UnresolvedAlias entries into a concrete Alias. This is done by taking the target and nesting and then resolving to what target the alias refers to, since at this point we finished indexing
    • Then we loop through all of the concrete aliases to figure out which constant the alias ultimately points to (since Ruby allows you to have an alias that points to another alias)

Automated Tests

Added tests that show the behaviour of the proposed API.

@vinistock vinistock added the enhancement New feature or request label Sep 18, 2023
@vinistock vinistock added this to the 2023-Q3 milestone Sep 18, 2023
@vinistock vinistock self-assigned this Sep 18, 2023
@vinistock vinistock requested a review from a team as a code owner September 18, 2023 20:14
@vinistock vinistock requested review from andyw8 and Morriar September 18, 2023 20:14
lib/ruby_indexer/lib/ruby_indexer/visitor.rb Outdated Show resolved Hide resolved
comments: T::Array[String],
).void
end
def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a lot of parameters, I wonder if we should use keywords instead? It may require some refactoring.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do the refactor in a different PR, since it will require quite a few changes.

sig { returns(String) }
attr_reader :target

sig { params(target: String, unresolved_alias: UnresolvedAlias).void }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the alias is "resolved", why not having it pointing to the constant directly? It would make following the aliases easier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we may have multiple entries for the same name. Having the resolved fully qualified name allows us to fetch those entries that are stored in the index.

lib/ruby_indexer/lib/ruby_indexer/index.rb Show resolved Hide resolved
lib/ruby_indexer/lib/ruby_indexer/visitor.rb Outdated Show resolved Hide resolved
lib/ruby_indexer/lib/ruby_indexer/index.rb Outdated Show resolved Hide resolved
lib/ruby_indexer/test/constant_test.rb Outdated Show resolved Hide resolved
@vinistock vinistock force-pushed the vs/handle_constant_aliases branch from 698c8b6 to a0788d9 Compare September 20, 2023 17:51
@vinistock vinistock requested a review from Morriar September 20, 2023 17:53
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])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
original_entries = T.must(@entries[entry.name])
original_entries = @entries.fetch(entry.name)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not a fan of that style, to be honest.

@vinistock vinistock force-pushed the vs/handle_constant_aliases branch from a0788d9 to dbf8ecf Compare September 20, 2023 20:50
@vinistock
Copy link
Member Author

vinistock commented Sep 20, 2023

Okay, I finally was able to realize what was missing for properly following aliases. When we are trying to resolve a name including namespaces, we need to recursively try to resolve possible aliases in the namespace to find the actual target.

So, if we take this alias from the Ruby LSP itself, this is the behavior now:

module RubyLsp
  Interface = LanguageServer::Protocol::Interface
end

# If we have a reference to `Interface`, then this will return the alias entry, allowing
# us to jump to the alias declaration
@index.resolve("Interface", ["RubyLsp"])
# => "RubyLsp::Interface"

# If we are reaching inside the aliased namespace, then we'll recursively resolved aliases
# along the name to ensure that we land on the right place. For example, for
# `Interface::Location`, we get
@index.resolve("Interface::Location", ["RubyLsp"])
# => "LanguageServer::Protocol::Interface::Location"

I believe this implementation makes sense. When there's an alias, the constant really exists so we can jump directly to it. When we are accessing inside an aliased namespace, then RubyLsp::Interface::Location indeed doesn't exist, it's the resolved alias (LanguageServer::Protocol::Interface::Location) that exists.

This PR already gets definition and hover working for aliases. After this goes in, I'll handle completion, which requires some extra work.

@vinistock vinistock requested a review from andyw8 September 20, 2023 20:55
@vinistock vinistock force-pushed the vs/handle_constant_aliases branch from dbf8ecf to 7d0beb1 Compare September 21, 2023 19:35
@vinistock
Copy link
Member Author

Okay, I fixed one remaining issue with following aliases. I think this should be good to review now.

return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)

name = fully_qualify_name(node.target.location.slice)
add_constant(node, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we missing the handling of constant target nodes? A, B = 42 and FOO::A, FOO::B = 42?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are indeed. Can I follow up with a separate PR for that? It will require some special handling because we need to take a look at the right hand side to figure out if they are aliases too.

So, we'll need to handle MultiWriteNode and the constant target nodes inside. I can create a PR immediately after this one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vinistock vinistock requested a review from Morriar September 22, 2023 17:32
@vinistock vinistock merged commit 048e879 into main Sep 25, 2023
28 of 30 checks passed
@vinistock vinistock deleted the vs/handle_constant_aliases branch September 25, 2023 13:52
vinistock pushed a commit that referenced this pull request Feb 28, 2024
…nd-patch-e63400fc73

Bump the minor-and-patch group with 2 updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Indexer: Handle other operator writers, e.g. &&=
3 participants