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:
- Plain
module Foo (utility/module_function modules) — no module node; its methods become orphan method nodes attached to the file node via contains.
Foo = Struct.new(...) constant assignment — no class node; block-defined methods attach to the file node.
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.
Summary
The Ruby extractor only creates class/module nodes for
class Foodeclarations. 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:module Foo(utility/module_functionmodules) — no module node; its methods become orphan method nodes attached to the file node viacontains.Foo = Struct.new(...)constant assignment — no class node; block-defined methods attach to the file node.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 .:Observed nodes (all 9)
No
TaxCalculator, noInvoice, noApiError, noBilling/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 viacontainsinstead of their container viamethod.Expected
module Foocreates a module node exactly likeclass Foodoes (tree-sitter's Ruby grammar exposesmodulenodes the same way).Struct.new(...),Class.new(...), orData.define(...)creates a class node named after the constant — these are the standard Ruby idioms for lightweight class definition.method, like class-declared methods.Why it matters together with #1634
#1634 covers edge resolution (
Const.methodcalls never resolve). This issue covers node extraction. They compound: in the repro,PaymentProcessor#processcallsTaxCalculator.rate_for, and that edge is doubly impossible — the resolver doesn't handle constant-receiver calls, and even if it did,TaxCalculatorhas 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.