Skip to content

PHP: member calls on a typed receiver produce no edge across files, and nothing across repos #3390

Description

@xiongjianxu

What happens

A PHP member call on a typed receiver produces no edge at all when the receiver's
class is declared in another file, and nothing survives into graph.json when it is
declared in another repository.

Two files, one corpus:

<?php // Greeter.php
class Greeter {
    public function greet(): void {}
}
<?php // App.php
class App {
    private Greeter $greeter;
    public function run(): void { $this->greeter->greet(); }
}

App.runGreeter.greet is missing. Two things have to be true for that:
member_call_expression's object field is never read, so the receiver is not even
recorded on the raw_call; and PHP exports no type table, so there would be nothing to
type it against. The call falls to raw_calls, the shared cross-file pass skips member
calls, and no resolver claims it. Every call through an injected dependency — the
dominant shape in any PHP framework — is invisible to affected and to every
reverse-dependency query.

Split the same code into two repos and merge:

repo app/  App.php      (as above)
repo lib/  Greeter.php

merge-graphs adds nothing, because the single-repo build never recorded that App.run
wanted a Greeter.greet it could not find. This is the PHP instance of #3152, whose merge
pass can only finish calls a build parked.

Expected

  • $this->greeter->greet() and $greeter->greet() resolve to Greeter.greet in one
    corpus (INFERRED — the type comes from a declaration, not from the call site).
  • When the receiver's type is declared nowhere in the build, the call is parked so
    merge-graphs / global add can bind it to the single declaration another repo owns.

Notes

All four places a PHP receiver's type is written down have to be read, or the common
shapes are missed: a typed property (private Greeter $g;), a promoted constructor
parameter (__construct(private Greeter $g), which declares no property at all), a typed
parameter, and $g = new Greeter().

Ambiguity must stay unresolved rather than be guessed: a union or intersection type
(Greeter|Other) names no one class, a chain longer than $this->prop->call() types no
receiver, and two classes named Greeter produce no edge.

Helper::format() is a scoped_call_expression, not a member call, and keeps its
existing path.

A paired PR follows.

Activity

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