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.run → Greeter.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.
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.jsonwhen it isdeclared in another repository.
Two files, one corpus:
App.run→Greeter.greetis missing. Two things have to be true for that:member_call_expression'sobjectfield is never read, so the receiver is not evenrecorded 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 membercalls, and no resolver claims it. Every call through an injected dependency — the
dominant shape in any PHP framework — is invisible to
affectedand to everyreverse-dependency query.
Split the same code into two repos and merge:
merge-graphsadds nothing, because the single-repo build never recorded thatApp.runwanted a
Greeter.greetit could not find. This is the PHP instance of #3152, whose mergepass can only finish calls a build parked.
Expected
$this->greeter->greet()and$greeter->greet()resolve toGreeter.greetin onecorpus (INFERRED — the type comes from a declaration, not from the call site).
merge-graphs/global addcan 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 constructorparameter (
__construct(private Greeter $g), which declares no property at all), a typedparameter, 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 noreceiver, and two classes named
Greeterproduce no edge.Helper::format()is ascoped_call_expression, not a member call, and keeps itsexisting path.
A paired PR follows.