Skip to content

fix(php): resolve method calls through $this-> properties on their declared type (#1220)#1251

Merged
colbymchenry merged 3 commits into
mainfrom
pr-1221-plus-fix
Jul 10, 2026
Merged

fix(php): resolve method calls through $this-> properties on their declared type (#1220)#1251
colbymchenry merged 3 commits into
mainfrom
pr-1221-plus-fix

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Fixes #1220. Carries #1221 by @w0lan (thank you for the excellent report and fix!) plus a hardening commit — landing directly so the fix ships without another review round-trip; all credit for the mechanism and validation corpus to @w0lan.

What #1221 contributed (carried verbatim, rebased onto main)

PHP method calls through a class property — $this->dep->method(), the dominant call shape in constructor-injection codebases — produced no calls edge. #1221 routes the this->prop.method shape exclusively through declared-type inference + resolveMethodOnType validation, and defers unresolved refs to the conformance pass so supertype-inherited methods resolve once extends/implements edges exist. 9 tests.

What the hardening commit adds

The original scan reused the generic local-variable patterns whole-file (the CFML component-scope treatment). In PHP a plain $prop local or parameter can never alias $this->prop, so a same-named variable in any method above the call could mistype the property and mint a wrong 0.9-confidence edge — reproduced with a local $greeter = new OtherGreeter() in an unrelated helper routing $this->greeter->greet() to OtherGreeter::greet despite the property being typed Greeter.

Now a this->prop receiver consults property-shaped declarations only:

  1. a modifier-prefixed typed declaration — covers typed properties (private ?Foo $prop;) and promoted constructor parameters (private readonly Foo $prop), and
  2. the pseudoconstructor assignment $this->prop = new Foo(), plus
  3. for classic untyped properties, following $this->prop = $var to $var's typed declaration bounded by the enclosing function — which also picks up multi-line constructor signatures and typed setter injection.

Union-typed and otherwise unrecoverable properties stay unlinked — a wrong inference produces no edge rather than a wrong one, per #1221's own design principle.

Validation

  • 15 tests in __tests__/php-property-receiver-resolution.test.ts (the original 9 unchanged + 3 interference regressions, multi-line ctor, setter injection, scope-boundary negative); full suite green (135 files, 2,324 passed).
  • Monolog: +53 property-receiver call edges vs the 1.4.0 baseline (parked this-> refs 430 → 377); every sampled edge routes to the property's true declared type.

Known limitations (unchanged from #1221): fluent second hops, nullsafe ?-> calls (missing from the extractor's callTypes entirely), docblock-only types. Note for users: existing indexes need a re-index to gain the new edges — the parked refs predate the #1240 retry ledger.

🤖 Generated with Claude Code

Roman Wolan and others added 3 commits July 8, 2026 20:30
…clared type (#1220)

A call whose receiver is a class property — $this->dep->method(), the
dominant call shape in DI-style PHP (Symfony/Laravel constructor
injection) — produced no call edge: the extractor records the receiver
as raw text (this->dep), which no resolution strategy could type, so
callers/impact silently missed every production consumer of a method
while reporting its unit-test callers (local-new receivers).

Three coordinated pieces, all riding existing machinery:

- inferLocalReceiverType: strip the PHP this-> prefix and widen the scan
  to the whole file — the same treatment CFML component-scoped fields
  already get. The existing PHP typed-parameter pattern then recovers
  the type from a promoted constructor parameter, a typed property
  declaration, or a classic constructor parameter alike.
- matchMethodCall: resolve the this->prop.method shape EXCLUSIVELY via
  declared-type inference + resolveMethodOnType validation; the
  name-similarity strategies never see it, so a property whose type
  can't be recovered stays unlinked rather than guessed.
- defer + conformance retry: a ref whose method lives on the property
  type's supertype resolves only once implements/extends edges exist —
  push it to the existing deferred-chain retry (PHP_PROP_SHAPE) and
  dispatch it back through matchMethodCall in the conformance pass.

Validation: 15-case corpus (promoted/classic/interface/inherited/
same-name-collision/untyped/deep-chain/local-shadowing) all resolve at
0.9 or stay deliberately unlinked; 9 new vitest tests; full suite
passes. On six real PHP codebases (PHP 7.1-8.5, Symfony 3.2-8.0) a
sampled repository method went 0/7 -> 7/7 production callers and 49/49
manually verified property-receiver edges were correct with no false
positives.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
…s only

The property-receiver resolution (#1220/#1221) reused the generic
local-variable patterns with a whole-file scan, CFML-style. In PHP a plain
$prop local or parameter can never alias $this->prop, so a same-named
variable in any method above the call could mistype the property and mint a
wrong 0.9-confidence call edge — e.g. a local $greeter = new OtherGreeter()
in an unrelated helper routed $this->greeter->greet() to
OtherGreeter::greet despite the property being typed Greeter.

A this->prop receiver now only consults property-shaped declarations: a
modifier-prefixed typed declaration (typed property or promoted constructor
parameter) and the $this->prop = new X() pseudoconstructor. Classic untyped
properties are typed by following the $this->prop = $var assignment to
$var's declaration within that function only — which also picks up
multi-line constructor signatures and typed setter injection. Union-typed
and otherwise unrecoverable properties stay unlinked, as before.

Validated on Monolog: 53 new property-receiver call edges, sampled edges all
route to the property's true declared type; parked this-> refs 430 -> 377.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PHP: method calls through a class property ($this->dep->method()) never resolve — constructor-injected dependencies produce no call edges

1 participant