Skip to content

Method-kind event-callback registrations (EventEmitter-style) aren't recognized as reachability roots #2259

Description

@carlos-alm

Summary

#2032 added a transitive-reachability check to the dead-code classifier. To avoid over-promoting genuinely-dead methods to "root" status (a gap Greptile flagged during that PR's review), isInterfaceDispatchMethodRoot (src/graph/classifiers/roles.ts, mirrored as is_interface_dispatch_method_root in crates/codegraph-core/src/graph/classifiers/roles.rs) only treats a method-kind, fan-in-0, hasActiveFileSiblings-rescued method as an unconditional reachability root when its bare name matches an actual interface/type declaration member somewhere in the codebase (e.g. enterNode matching interface Visitor { enterNode?(...): ...; }).

This correctly narrows the false-positive class Greptile identified (an ordinary unused class method with no relation to any interface). But it also means a method-kind callback registered with a runtime event emitter (Node's Worker/EventEmitter .on('event', cb) pattern, not a TypeScript-declared interface) no longer qualifies as a root, since there's no local interface/type declaration for codegraph to match against.

Confirmed real instance (self-build)

WasmWorkerPool.onMessage in src/domain/wasm-worker-pool.ts:276 is a genuine, live method (presumably wired up via worker.on('message', ...) or similar) with fanIn === 0 — nothing in the graph resolves a call to it, since Node's built-in EventEmitter registration isn't a TypeScript interface member. Post-#2032, it is correctly still classified leaf itself (via the existing hasActiveFileSiblings rescue, untouched by the reachability downgrade), but it is no longer promoted to root status, so its own callees are wrongly flagged dead:

  • deserializeResult, deserializeCoreFields, deserializeBindingFields, deserializeMapFields (all in wasm-worker-pool.ts) — called only by onMessage, now dead-unresolved.

Checked: onMessage does not repeat across multiple files in this codebase (ruled out a "same name in ≥2 files" heuristic as an easy fix — it wouldn't help here, since this is a singular occurrence, structurally indistinguishable from a genuinely dead singular method by name-repetition alone).

Suggested fix

One of:

  1. Give well-known Node.js/framework event-registration patterns (.on('event', methodRef), EventEmitter subclass method binding, etc.) a real calls-style edge from the registration call site to the bound method, the same way Dispatch-table function references (resolve: fn) inconsistently flagged dead-unresolved depending on unrelated fanOut #1771/roles --role dead: object-literal property-value references count as liveness without checking if the property is ever invoked (false negative) #1895 did for object-literal value-refs — the most precise fix, but real extraction work.
  2. Recognize a narrower, still-safe root signal for methods bound as a callback argument to a recognized emitter API (.on(, .addEventListener(, .once() even without a matching interface declaration — cheaper, but needs care to avoid reopening the exact "ordinary unused method" gap roles --role dead: non-transitive fan-in means a function called only by another dead function is never flagged dead #2032 just closed.

Either fix needs the equivalent change in both src/graph/classifiers/roles.ts and crates/codegraph-core/src/graph/classifiers/roles.rs for dual-engine parity.

Related

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