Split out from the review of upstream Graphify-Labs#2536, where an automated reviewer objected that #39's refusal drops information (thread). The objection to the proposed alternative was declined with measurements (below), but the underlying gap it points at is real and currently untracked.
Current behaviour (correct, but incomplete)
#39 / 075038e refuses parent, self and static as PHP scoped-call callee names (graphify/extractors/engine.py:4382-4394, gated on _PHP_RELATIVE_SCOPE_NAMES). Before it, the handler took the scope text as the callee, so every parent::setUp() emitted a raw call named parent, and the cross-file label pass bound them all to whatever ->parent() accessor existed — 1,698 fabricated calls edges onto ServiceCategory::parent() on the pinned 46.4k-node corpus.
The refusal removes the fabricated edges and invents no replacement. So parent::__construct(), parent::setUp(), self::helper() and static::make() currently produce no calls edge at all, with no diagnostic counter.
Why the obvious alternative does not work
Emitting the method name as an unqualified callee instead — the reviewer's suggestion — was implemented and measured against a PHPUnit-shaped corpus (base class out of corpus, one unrelated in-corpus setUp):
- baseline: 0 edges
- unqualified-callee variant: 1 fabricated INFERRED edge,
FooTest::prepare() → App\Models\ServiceCategory::setUp()
The raw call carries is_member_call=False, so it is skipped by neither the member-call guard nor the typed resolver, and reaches the shared normalized-label loop whose index keys .setUp() and setUp() identically → sole candidate → unconditional bind. #52's candidate filter does not cover it (verified by building a tree with #52 applied — still 1 edge): that filter is gated on the php_function_call marker, and #52's own comment explains it cannot be widened to scoped calls, because a scoped_call is also not a member call.
What the real fix needs
The rc_entry built for a raw call carries caller, callee, receiver, receiver type and language — no enclosing class and no base class. The information that would answer "which class does parent denote here" exists in the graph as inherits edges (minted by _php_emit_base), but the shared bare-label loop never consults them.
So a correct fix is a resolution-pass change, not an extractor tweak:
- Carry the enclosing class on the raw call (or recover it from
caller_nid).
- For a relative scope, resolve it against the caller's own
inherits edge — parent → the resolved base class, self/static → the enclosing class itself (late static binding means static is strictly a guess for anything but the declaring class; refusing static while resolving parent/self is a defensible first cut).
- Look the method up on that resolved type, and refuse when the base class is out of corpus (the PHPUnit case) rather than falling back to a name match.
Point 3 is the load-bearing one: an out-of-corpus base class must yield nothing, or this reintroduces exactly the fabricated-edge shape it replaces.
Notes
- Sequencing: composes with
#52's candidate-filtering, which lives on fix/resolution-gaps-52-53-54. Worth landing after that branch merges to v8.
- Extraction-side, so it will need a version bump to roll the AST cache namespace.
- Blast radius if fixed:
parent::__construct() and parent::setUp() are ubiquitous in a Laravel/PHPUnit corpus, so this is recall left on the table, not a correctness bug.
Split out from the review of upstream Graphify-Labs#2536, where an automated reviewer objected that
#39's refusal drops information (thread). The objection to the proposed alternative was declined with measurements (below), but the underlying gap it points at is real and currently untracked.Current behaviour (correct, but incomplete)
#39/075038erefusesparent,selfandstaticas PHP scoped-call callee names (graphify/extractors/engine.py:4382-4394, gated on_PHP_RELATIVE_SCOPE_NAMES). Before it, the handler took the scope text as the callee, so everyparent::setUp()emitted a raw call namedparent, and the cross-file label pass bound them all to whatever->parent()accessor existed — 1,698 fabricatedcallsedges ontoServiceCategory::parent()on the pinned 46.4k-node corpus.The refusal removes the fabricated edges and invents no replacement. So
parent::__construct(),parent::setUp(),self::helper()andstatic::make()currently produce nocallsedge at all, with no diagnostic counter.Why the obvious alternative does not work
Emitting the method name as an unqualified callee instead — the reviewer's suggestion — was implemented and measured against a PHPUnit-shaped corpus (base class out of corpus, one unrelated in-corpus
setUp):FooTest::prepare()→App\Models\ServiceCategory::setUp()The raw call carries
is_member_call=False, so it is skipped by neither the member-call guard nor the typed resolver, and reaches the shared normalized-label loop whose index keys.setUp()andsetUp()identically → sole candidate → unconditional bind.#52's candidate filter does not cover it (verified by building a tree with#52applied — still 1 edge): that filter is gated on thephp_function_callmarker, and#52's own comment explains it cannot be widened to scoped calls, because ascoped_callis also not a member call.What the real fix needs
The
rc_entrybuilt for a raw call carries caller, callee, receiver, receiver type and language — no enclosing class and no base class. The information that would answer "which class doesparentdenote here" exists in the graph asinheritsedges (minted by_php_emit_base), but the shared bare-label loop never consults them.So a correct fix is a resolution-pass change, not an extractor tweak:
caller_nid).inheritsedge —parent→ the resolved base class,self/static→ the enclosing class itself (late static binding meansstaticis strictly a guess for anything but the declaring class; refusingstaticwhile resolvingparent/selfis a defensible first cut).Point 3 is the load-bearing one: an out-of-corpus base class must yield nothing, or this reintroduces exactly the fabricated-edge shape it replaces.
Notes
#52's candidate-filtering, which lives onfix/resolution-gaps-52-53-54. Worth landing after that branch merges tov8.parent::__construct()andparent::setUp()are ubiquitous in a Laravel/PHPUnit corpus, so this is recall left on the table, not a correctness bug.