Skip to content

Ruby: namespaced-constant receivers (A::B.call) get no references edge, so the class has 0 callers #2503

Description

@james-jang

Summary

A Ruby call whose receiver is a namespaced constant (Billing::ChargeCard.call(...)) gets no references edge to the class, so the class shows 0 callers / 0 dependents even though it is called from many places. #1110 added the receiver references edge for a bare constant (ChargeCard.call), but the scope_resolution form is excluded.

This pattern is the norm in Rails apps that organize service objects or commands under namespaces (app/services/billing/charge_card.rb → Billing::ChargeCard.call(...)). In the Rails codebase we evaluated (v1.6.2), it accounts for roughly 1.6k call sites, and callers / impact / codegraph_explore returned nothing for any of the classes we checked.

Repro (v1.6.2)

# app/services/billing/charge_card.rb
module Billing
  class ChargeCard
    def self.call(**kw) = new(**kw).call
    def initialize(amount:) = @amount = amount
    def call = @amount
  end
end

# app/services/checkout/complete.rb
module Checkout
  class Complete
    def call
      Billing::ChargeCard.call(amount: 1)   # namespaced receiver: no edge
    end
  end
end

# app/services/billing/refund.rb
module Billing
  class Refund
    def call
      ChargeCard.call(amount: 2)            # bare receiver: edge recorded
    end
  end
end

Actual output (v1.6.2, fresh codegraph init):

$ codegraph callers ChargeCard
Callers of "ChargeCard" (1):

class Billing::ChargeCard (ruby) — app/services/billing/charge_card.rb:2
method      call [references]
  app/services/billing/refund.rb:3          <- only the bare-receiver caller

$ codegraph callees call -f checkout/complete.rb
Callees of "call" (0):

method Checkout::Complete::call (ruby) — app/services/checkout/complete.rb:3
ℹ No callees found for "call"

$ codegraph callees call -f billing/refund.rb
Callees of "call" (2):
...
class       ChargeCard [references]
  app/services/billing/charge_card.rb:2

Expected: Checkout::Complete#call is listed as a caller of Billing::ChargeCard, the same as Billing::Refund#call.

Root cause

codegraph-kernel/src/ruby.rs emits the extra receiver references ref only when the receiver node kind is constant. The comment says so explicitly:

// Capitalized constant receiver (`Klass.static_call`, `RETRY_MAX.times`)
// → an ADDITIONAL references ref at the RECEIVER's position.
// (scope_resolution receivers get none — type ≠ constant.)
if !skip && receiver.kind() == "constant" {
self.push_ref_at(
caller,
&receiver_name.to_string(),
edge_kind_index("references").unwrap(),
receiver,
);
}

// Capitalized constant receiver (`Klass.static_call`, `RETRY_MAX.times`)
// → an ADDITIONAL references ref at the RECEIVER's position.
// (scope_resolution receivers get none — type ≠ constant.)
if !skip && receiver.kind() == "constant" {

Suggested fix

Also emit the references ref when receiver.kind() == "scope_resolution" and its last segment is capitalized. Either:

  • reference the full qualified name (Billing::ChargeCard), resolving against the class node's qualified name, which is the precise option since last segments like Create / Update repeat across namespaces; or
  • reference the last segment, mirroring how NS::Widget.new → instantiates Widget is already handled a few lines above (L796-L811), and let the resolver disambiguate.

The first is preferable for Rails/Zeitwerk codebases, where the fully qualified constant maps one-to-one to a file.

Activity

  1. added a commit that references this issue on Oct 23, 2026
    84ef2a2
  2. colbymchenry commented on Oct 23, 2026

    @colbymchenry
    Owner

    Thanks for the clear report and repro!

    #2827 is merged to main. A call on a namespaced class like Billing::ChargeCard.call(...) or ::Billing::ChargeCard.call(...) now links the caller to the class and to the class method it calls, so codegraph callers ChargeCard lists Checkout::Complete#call too.

    The name is looked up the way Ruby does it, from the namespaces around the call outward. So Users::Create and Billing::Create aren't confused, and library constants like ActiveRecord::Base never land on a project class of the same last name.

    Thanks also to @drakeo338, whose #2509 was helpful guidance. It ships in the next release; re-index your Ruby projects after upgrading.

  3. added a commit that references this issue on Oct 23, 2026
    dcb985f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions