Skip to content

PHP: inter-class member calls are silently dropped, and language builtins (e.g. empty()) get mis-wired to same-name internal methods #3830

Description

@albanlapakti

Environment

  • graphify 0.9.65 (also reviewed 0.9.66/0.9.67 release notes — no PHP member-call resolver mentioned)
  • Corpus: a ~52 MB Symfony PHP bundle (11,007 nodes / 32,817 edges after extraction), --code-only mode

Problem A — PHP member calls are silently dropped

extract.py ships member-call resolvers for Swift, Python, TypeScript, C++, C#, Java… but there is no _resolve_php_member_calls. A PHP call like:

$this->emailSender->prepareAndSendEmail($action);

produces no edge at all. Empirical check on our graph: EmailSender::prepareAndSendEmail has 0 incoming edges from other files while at least two services call it — all 18 of its incoming calls edges are intra-file self-calls.

Notably, the cross-repo parking mechanism already exists (_park_unresolved_member_call -> metadata.unresolved_calls, ref #3152) but is never invoked for PHP. Our full graph contains zero parked entries with "lang": "php" — the calls are dropped with no trace instead of being parked.

Problem B — builtins get mis-wired to same-name internal methods

_rewire_unique_stub_nodes rebinds a call to the single internal method sharing the callee's name. PHP language builtins collide with that rule:

  • ~200 calls to empty() (language construct) across ~150 files are wired to an internal method ParseCollectionPaginator::empty() — including 24 false edges from two god-node services alone.
  • Same risk class for common names: execute, handle, format.

So the graph is wrong in both directions: missing edges on real inter-class calls, polluted edges on builtin names.

Impact on a real corpus

A local post-processor (tree-sitter-php, same parking format) recovered on this single bundle:

  • -454 false edges removed (builtin rewiring)
  • +1,566 real edges added (typed-property member calls, interface -> implementations)
  • 6,558 calls parked in metadata.unresolved_calls (previously dropped silently)

Suggested fix

  1. Add a PHP member-call resolver: receiver type from typed/promoted constructor properties + $this->method() self-calls; interface calls -> all implementations.
  2. Park unresolved PHP member calls in metadata.unresolved_calls (same payload format as other languages, "lang": "php") so merge-graphs can finish the edges.
  3. Never rewire a builtin callee name (empty, count, strlen, ...) to an internal method — consult a builtin list per language before rewiring.

Repro

Any PHP corpus containing $this->someTypedProperty->method(): the edge is absent from graph.json and nothing appears in metadata.unresolved_calls for the caller. Then add a second method named empty anywhere in the corpus and observe empty() calls being wired to it.

Happy to share our local post-processor (pure post-processing on graph.json, idempotent) if useful as a reference implementation.

Activity

  1. safishamsi commented on Oct 4, 2026

    @safishamsi
    Member

    Fixed in v0.9.76 (live on PyPI as graphifyy==0.9.76) via #3975, thanks @Cintu07. 🙏

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions