Shopify / Shopify/rubydex

Singleton references to non-namespace constants are never retired

Open
#1,012 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
355
Forks
24
Avg merge
2d 6h
Merged PRs (30d)
17

Description

Summary

The indexer emits a <Receiver> singleton constant reference for every method call whose receiver is a constant (indexing/ruby_indexer.rs, the format!("<{}>", ...) path). When that receiver resolves to a Constant or ConstantAlias rather than a class or module, there is no singleton class for the reference to find, so it can never resolve. It stays in pending_work and is retried on every resolution pass, for the life of the graph.

Reproduction

module Gateway
  API_VERSION = "9.5"

  def self.header
    "application/PTI#{API_VERSION.delete('.')}"
  end
end

Index that source, resolve, then inspect the pending work.

Observed:

declarations in the graph:
  Gateway (Module)
  Gateway::<Gateway> (SingletonClass)
  Gateway::<Gateway>#header() (Method)
  Gateway::API_VERSION (Constant)

after the first resolve: 1 pending item(s)
  unresolved reference `<API_VERSION>`  ->  attached name resolved to: Constant `Gateway::API_VERSION`

after five consecutive resolves, still pending: 1 item(s)
  unresolved reference `<API_VERSION>`  ->  attached name resolved to: Constant `Gateway::API_VERSION`

The interesting part is the second half of that line. The attached name API_VERSION did resolve, to a Constant. The resolver therefore already knows the receiver is not a namespace at the moment it decides to re-queue the singleton reference.

Expected

Once the attached name resolves to a declaration that cannot own a singleton class, the singleton reference should be retired rather than re-queued.

Actual

It is re-queued on every pass and retried indefinitely.

Impact

Measured on Core, fully indexed including RBS, rubygems and git-sourced gems
(138,503 documents, 1,651,554 declarations):

Count
Total pending work after a full resolve 32,212
Unresolved <Foo> singleton references 16,754
— attached name also unresolved (legitimately queued) 4,394
attached name resolved to Constant 11,913
attached name resolved to ConstantAlias 447

12,360 items, 38% of the whole pending queue, are permanently unresolvable and permanently
retried.

This is not exotic code. Every one of these comes from ordinary method calls on constants:

  • "application/PTI#{API_VERSION.delete('.')}" — activemerchant orbital.rb:39
  • '0' => STANDARD_ERROR_CODE[:processing_error] — activemerchant adyen.rb:25
  • YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = ... — rails active_record.rb:662

Why it is worth fixing

Incremental resolution cost. On the same workspace, an in-memory edit to one file disturbs only
31 entities (6 definitions, 25 constant references, 0 ancestor chains), and resolving that
takes 27 ms. But a resolve after loading a persisted graph walks the whole backlog instead,
because these entries are still in it. Retiring them cuts the backlog by more than a third
without changing what resolves.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in indexing/ruby_indexer.rs at the format!("<{}>", ...) path, then trace singleton references through resolution and pending_work handling. Reproduce the Gateway example, resolve it repeatedly, and inspect the pending queue; done means references whose attached name resolves to Constant or ConstantAlias are retired instead of remaining queued.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby, rust
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.