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

Eliminate dependency on zeitwerk #26

Open
exterm opened this issue Oct 14, 2021 · 0 comments
Open

Eliminate dependency on zeitwerk #26

exterm opened this issue Oct 14, 2021 · 0 comments

Comments

@exterm
Copy link
Contributor

exterm commented Oct 14, 2021

Currently this gem guesses the file that defines a constant by following zeitwerk's mapping conventions in reverse. In consequence, it only works reliably on code bases that use zeitwerk for loading.

If we could sever this dependency and use a different mechanism to resolve constants,

  • we'd have a first step towards analyzing code that is not zeitwerk-loaded (like gems, or a Rails app's lib folder)
  • we wouldn't have to know about custom inflections to resolve constants, which is a major source of complexity for both constant_resolver and its main user, packwerk.
  • we wouldn't have to know the application's load paths to resolve constants

Possible alternative resolution methods to consider

Tracepoints

Tracepoints allow registering callbacks for certain events in the Ruby interpreter. While there is no tracepoint for defining constants, there is one for defining classes or modules. We could register this tracepoint, then load all the code that could possibly define the constant we're interested in, building up a lookup table from tracepoint invocations, and use that to resolve the constant.

Drawbacks:

  • unclear whether this will fire for class reopening (to be investigated)
  • requires loading the whole codebase to be accurate
  • tracepoint needs to be registered before any code from the interrogated application is loaded, creating added complexity through a load time dependency

Module.const_source_location

In Ruby 2.7, a new method was added that allows looking up the location a (loaded) constant is defined in. We can resolve constants in context by trying the enclosing namespaces first:

if "Sales::Entry".safe_constantize
  return Sales.const_source_location("Entry")
end
if "Entry".safe_constantize
  return Module.const_source_location("Entry")
end

Caveat: const_source_location only returns the file that defines the constant, no files that reopen classes or modules. If we have multiple files with a module Sales statement, which one is the definition depends on load order, which is usually unpredictable in large applications. We can work around this by making sure we've loaded all relevant code, then inspecting Entry for all the methods and constants it contains, ask for their source locations, and get the full list of files defining or reopening the module / class.

Advantages:

  • Works just fine if the constant is already loaded before constant_resolver is loaded or invoked, so no load order dependency between the application and the gem

Disadvantages:

  • requires loading the whole codebase to be accurate

Sorbet

Sorbet already finds and resolves all constant references for its type analysis. We should be able to hook into it for constant resolution, and potentially, in packwerk, also for finding references.

We'd probably start up a language server, let it run an initial analysis of the codebase to build up context. Afterwards, asking the LS some questions (e.g. for constant resolution) should be relatively quick.

Advantages:

  • understands everything that sorbet understands
  • no need to load the application

Disadvantages:

  • We'd need to boot a language server once and keep it around, which would be a major change to how constant_resolver's lifecycle works. The full integration into packwerk would include swapping out the parsing and AST analysis steps, so would be a major rewrite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant