Skip to content

PHP: app(X::class)->method() misresolves to the *enclosing* class's own method of the same name (wrong edge, not a missing edge) #76

Description

@filipechagas

Found while validating the fork on providers-api.lawnstarter.com for PE-80051 (adoption epic).

Version: graphify 0.9.40, fork origin/v8 = 5e9d95c11837aeabbbb5d90cb45544f49dc58ca4.

Summary

The documented limit is that service-locator calls "never resolve" — no static tool can bind app(X::class). That would be acceptable: a missing edge is a known unknown.

What actually happens is worse. app(Handler::class)->handle() inside a class that itself has a handle() method produces a calls edge to its own handle(), not to Handler::handle(). The graph asserts a call that does not exist, and the real callee gets no incoming edge at all.

A consumer cannot detect this: the edge looks exactly like a legitimately resolved call.

Minimal reproduction

// src/Handler.php
<?php
namespace App;

class Handler
{
    public function handle(): string { return 'handled'; }
}
// src/Job.php
<?php
namespace App;

class Job
{
    public function handle(): string { return 'job-handle'; }

    public function failed(): string { return app(Handler::class)->handle(); }
}
$ graphify update .
$ python3 -c "…dump calls edges…"
.failed() src/Job.php -> .handle()  TARGET_SRC=src/Job.php L6  | at src/Job.php L8

The edge from Job::failed() (L8) points at Job::handle() (L6, same file) instead of Handler::handle(). Handler has no incoming calls edge whatsoever.

Expected: either no edge at all (honest refusal), or an indirect_call edge to Handler::handle().

Impact on the real repo

Reproduces in both Laravel job classes, in exactly the pattern Laravel encourages for failed() handlers:

Call site Real callee Edge graphify emitted
EvaluateAssessmentJob::failed() L46 — app(HandleFailedAssessmentEvaluation::class)->handle(...) HandleFailedAssessmentEvaluation::handle EvaluateAssessmentJob::handle() (L39, own class)
ScoreAssessmentAnswerJob::failed() L49 — app(HandleFailedAssessmentScoring::class)->handle(...) HandleFailedAssessmentScoring::handle ScoreAssessmentAnswerJob::handle() (L42, own class)

Neither HandleFailedAssessmentEvaluation nor HandleFailedAssessmentScoring has a single incoming calls edge in the graph — so "what breaks if I change the failure handler?" returns a false negative, while "what does failed() call?" returns a false positive pointing back into the job itself.

Suggested fix

When the receiver expression is a container call (app(...), resolve(...), App::make(...)), suppress method-name resolution against the enclosing class scope. If the ::class argument is a literal, an indirect_call edge to that class's method would be recoverable and strictly better; otherwise emit nothing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions