Singleton references to non-namespace constants are never retired
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('.')}"— activemerchantorbital.rb:39'0' => STANDARD_ERROR_CODE[:processing_error]— activemerchantadyen.rb:25YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = ...— railsactive_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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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