Sitelet https://github.com/Graphify-Labs/graphify/issues/3077
Skip to content

Ruby: foo!/foo? collide with foo in node IDs — the bang/predicate variant is silently dropped #3077

Description

@jplot

Summary

Node IDs normalise a symbol name to [a-z0-9_], which strips Ruby's ! and ? suffixes. A bang or
predicate method therefore produces the same ID as its non-suffixed sibling defined in the same file,
and the dedup pass (if n['id'] not in seen) keeps whichever was encountered first. The other method
disappears from the graph entirely — no node, and no edges.

The ID rule in references/extraction-spec.md says the entity part is "the symbol name similarly
normalized", with [a-z0-9_] as the allowed set. save_it and save_it! both normalise to save_it.

Evidence

Measured on a Rails API-only codebase (graphifyy 0.9.48, undirected build, ~420 files):

  • def definitions found under app/: 631
  • method nodes in graph.json for app/: 626
  • gap: exactly 5

All five are ID collisions — four foo / foo! pairs and one foo / foo? pair, spread over three model
files. They account for 100% of the extraction loss: once the collisions are excluded, every remaining
def in the corpus has a matching node, so there is no other failure mode at play.

Why it matters more than 0.8% suggests

The bias is directional. In Ruby the bang variant is conventionally the persisting / raising one, and
it is the public entry point that callers actually use:

def attach(thing)
  self.thing = thing
end

def attach!(thing)   # <- dropped from the graph
  attach(thing)
  save!
end

The graph keeps the private setter and hides the public API. Any traversal, explain, or path query
over such a class will miss the persisting method, and its callers show up as isolated nodes.

The same applies to predicates: the foo? guard is dropped in favour of the foo value method.

Reproduction

# app/models/thing.rb
class Thing < ApplicationRecord
  def save_it
    self.state = :done
  end

  def save_it!
    save_it
    save!
  end
end

graphify . produces one node for save_it and none for save_it!.

Suggested fix

Transliterate rather than strip, in the entity part of the ID only:

  • !_bang
  • ?_p (or _q)
  • =_eq (setters have the same problem: foo vs foo=)

save_it!app_models_thing_save_it_bang. This keeps the ID charset at [a-z0-9_], stays deterministic
from the label alone, and is a pure win — no existing non-suffixed ID changes, so previously built graphs
keep matching on --update for every method that has no suffixed sibling.

Ruby is the obvious case, but the same normalisation affects any language with suffixed identifiers
(Scheme/Clojure foo?/foo!, Ruby setters foo=).

Environment

  • graphifyy 0.9.48, skill 0.9.20
  • corpus: ~420 files, Rails API-only app, 1773 nodes / 2161 edges
  • undirected build, --update incremental

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions