Repository navigation
Description
Summary
In PHP, a method call whose receiver is a class property — $this->greeter->greet() — never produces a calls edge, regardless of whether the property is typed with a concrete class or an interface, and regardless of declaration style (PHP 8 constructor property promotion or a classic typed property assigned in the constructor).
Receiver-type inference added in #1108 (locals) and #1125 (parameters) covers the other shapes: on 1.3.0, local-variable receivers ($g = new Greeter(); $g->greet()) and typed-parameter receivers (function run(Greeter $g) { $g->greet(); }) both resolve correctly. Class-property receivers are the missing case — and in dependency-injection codebases (Symfony, Laravel constructor injection) they are the dominant call shape: nearly every cross-class call goes through a constructor-injected property.
Evidence
Minimal repro — 4 call sites of Greeter::greet(), identical except for how the receiver is obtained:
// src/Greeter.php
class Greeter {
public function greet(string $n): string { return "hi $n"; }
}
// src/UserViaNew.php — local variable
class UserViaNew {
public function run(): string { $g = new Greeter(); return $g->greet('new'); }
}
// src/UserViaParam.php — typed parameter
class UserViaParam {
public function run(Greeter $greeter): string { return $greeter->greet('param'); }
}
// src/UserViaDi.php — promoted constructor property
class UserViaDi {
public function __construct(private readonly Greeter $greeter) {}
public function run(): string { return $this->greeter->greet('di'); }
}
// src/UserViaClassicProp.php — classic typed property
class UserViaClassicProp {
private Greeter $greeter;
public function __construct(Greeter $greeter) { $this->greeter = $greeter; }
public function run(): string { return $this->greeter->greet('classic'); }
}codegraph init && codegraph callers greet (v1.3.0, also reproduced on 1.2.0):
Callers of "greet" (2):
method run
src/UserViaNew.php:3
method run
src/UserViaParam.php:3
| Receiver shape | Resolves? |
|---|---|
local variable from new |
✅ (#1108) |
| typed function parameter | ✅ |
promoted constructor property (private readonly Greeter $greeter) |
❌ |
classic typed property (private Greeter $greeter; + constructor assignment) |
❌ |
Same-class calls ($this->method()) and static calls resolve fine; this is specifically the property-receiver hop.
Why it matters
Measured on six real PHP codebases (340 → 9 200 files, PHP 7.1 → 8.5, Symfony 3.2 → 8.0): in DI-style code every cross-class production call goes through constructor-injected properties, so codegraph_callers returned 0 of 13 verified production call sites across the methods we first sampled, while happily returning the unit-test call sites (local new receivers). The result is worse than an empty answer: a method with production callers looks test-only/dead, and codegraph_impact reports only same-name declarations, so blast-radius on a signature change misses every real consumer. (Happy to share the detailed measurement protocol.)
I have a working fix (PR incoming) that follows the existing receiver-type-inference architecture; validated on those six codebases it takes a sampled repository method from 0/7 to 7/7 production callers with 49/49 manually verified call edges and no false positives — details in the PR.
Root cause (from reading main)
- Extraction (
src/extraction/tree-sitter.ts,extractCall): formember_call_expressionthe receiver becomes the node's raw text with only the leading$stripped, so$this->greeter->greet()is recorded as the referencethis->greeter.greet. Java has a dedicated unwrap for exactly this shape —field_accesswith athisobject unwraps to the bare field name, with the comment "so the resolver can then look up in the enclosing class's field declarations" — but the condition only matches Java'sfield_access; PHP'smember_access_expressionnever takes that path. - Resolution (
src/resolution/): nothing splits->, and nothing consults class property declarations for PHP — sothis->greeteris an unmatchable receiver string and the reference is dropped.
The type information is statically available: promoted properties carry it in the constructor signature, classic typed properties (PHP 7.4+) in the declaration, and the PHP extractor already collects property_declaration nodes (fieldTypes in src/extraction/languages/php.ts) — it's just not used for receiver typing.
Relationship to existing work
- Local-variable receiver-type inference is C++-only — instance method calls through a local don't resolve in other languages #1108 (locals) and TypeScript/JavaScript: local-variable receiver-type inference doesn't cover typed function parameters (unlike Java/C#/Kotlin/Swift/Scala) #1125 (parameters) fixed the sibling shapes; this is the third leg.
- CFML got exactly this in 1.3.0 (component properties, pseudoconstructor pattern — per the changelog "the same receiver-type inference the other object-oriented languages already had").
- PR feat(resolution): resolve this.<field>.method() on the field's declared type (TS/JS) #920 (open) implements
this.<field>.method()resolution on the field's declared type for TS/JS — this issue is the PHP counterpart.
Environment: CodeGraph 1.2.0 and 1.3.0 (npm), macOS arm64, PHP grammar as bundled.
Activity
fix(php): resolve method calls through $this-> properties on their de…