Skip to content

Ruby: no nodes extracted for plain modules or Struct.new/Class.new constant assignments — their call edges are impossible even with resolution fixed #1640

Description

@krishnateja7

Summary

The Ruby extractor only creates class/module nodes for class Foo declarations. Three very common Ruby shapes produce no node at all, which makes every edge into them impossible — even if edge resolution (#1634) were fixed, there would be nothing to attach the edge to:

  1. Plain module Foo (utility/module_function modules) — no module node; its methods become orphan method nodes attached to the file node via contains.
  2. Foo = Struct.new(...) constant assignment — no class node; block-defined methods attach to the file node.
  3. Foo = Class.new(StandardError) (the idiomatic one-line error class) — no node of any kind.

In service-object-style Ruby codebases these shapes are pervasive (Result structs, error classes, stateless utility modules), so a meaningful slice of the call graph has no possible target.

Minimal reproduction

4 files, then graphify update .:

# app/services/payment_processor.rb  — CONTROL
class PaymentProcessor
  def process(order)
    TaxCalculator.rate_for(order)
  end
end

# app/services/tax_calculator.rb  — shape 1
module TaxCalculator
  module_function

  def rate_for(order)
    0.2
  end
end

# app/services/invoice.rb  — shapes 2 + 3
Invoice = Struct.new(:total, :tax) do
  def grand_total
    total + tax
  end
end

ApiError = Class.new(StandardError)

# app/services/nested_util.rb  — shape 1, nested
module Billing
  module Rounding
    module_function

    def round(x)
      x.round(2)
    end
  end
end

Observed nodes (all 9)

label="invoice.rb"           file=app/services/invoice.rb
label="grand_total()"        file=app/services/invoice.rb
label="nested_util.rb"       file=app/services/nested_util.rb
label="round()"              file=app/services/nested_util.rb
label="payment_processor.rb" file=app/services/payment_processor.rb
label="PaymentProcessor"     file=app/services/payment_processor.rb   <-- only the control got a node
label=".process()"           file=app/services/payment_processor.rb
label="tax_calculator.rb"    file=app/services/tax_calculator.rb
label="rate_for()"           file=app/services/tax_calculator.rb

No TaxCalculator, no Invoice, no ApiError, no Billing/Rounding. Note the secondary symptom: methods of node-less containers get dot-less labels (rate_for() vs the class-owned .process()) and hang off the file node via contains instead of their container via method.

Expected

  • module Foo creates a module node exactly like class Foo does (tree-sitter's Ruby grammar exposes module nodes the same way).
  • Constant assignment where the RHS is Struct.new(...), Class.new(...), or Data.define(...) creates a class node named after the constant — these are the standard Ruby idioms for lightweight class definition.
  • Their methods attach to the container node via method, like class-declared methods.

Why it matters together with #1634

#1634 covers edge resolution (Const.method calls never resolve). This issue covers node extraction. They compound: in the repro, PaymentProcessor#process calls TaxCalculator.rate_for, and that edge is doubly impossible — the resolver doesn't handle constant-receiver calls, and even if it did, TaxCalculator has no node. Fixing #1634 alone leaves every plain-module/Struct target unreachable.

Environment: graphifyy 0.9.5 via uv, Ruby files parsed with the bundled tree-sitter grammar, macOS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions