Skip to content

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

Open
w0lan wants to merge 1 commit into
colbymchenry:mainfrom
w0lan:fix/php-property-receiver-type
Open

fix(php): resolve method calls through $this-> properties on their declared type (#1220)#1221
w0lan wants to merge 1 commit into
colbymchenry:mainfrom
w0lan:fix/php-property-receiver-type

Conversation

@w0lan

@w0lan w0lan commented Jul 8, 2026

Copy link
Copy Markdown

Fixes #1220

Problem

A PHP method call whose receiver is a class property — $this->dep->method(), the dominant call shape in constructor-injection codebases (Symfony, Laravel) — produces no calls edge. The extractor records the receiver as raw text (this->dep), which no resolution strategy can type, so the reference is dropped. codegraph_callers then reports a heavily-used method as uncalled (or test-only, since unit tests calling through local new receivers DO resolve), and impact analysis misses every production consumer. Full evidence and measurement protocol in #1220.

Fix

Three coordinated pieces, all riding existing machinery (no schema or extraction changes):

  1. inferLocalReceiverType (name-matcher.ts): strip the PHP this-> prefix and widen the scan to the whole file — the same treatment CFML component-scoped fields (variables.svc / this.svc) already get in this function. The existing PHP typed-parameter pattern (Foo $prop) then recovers the type from a promoted constructor parameter, a typed property declaration, or a classic constructor parameter alike; nearest-declaration-backward still lets a shadowing local win.
  2. matchMethodCall (name-matcher.ts): resolve the this->prop.method shape exclusively through declared-type inference + resolveMethodOnType validation at confidence 0.9 (the same tier as the Local-variable receiver-type inference is C++-only — instance method calls through a local don't resolve in other languages #1108/TypeScript/JavaScript: local-variable receiver-type inference doesn't cover typed function parameters (unlike Java/C#/Kotlin/Swift/Scala) #1125 typed paths). The name-similarity strategies below never see this shape, so a property whose type can't be recovered statically (docblock-only, setter/container injection) stays unlinked rather than guessed — a wrong inference produces no edge rather than a wrong one.
  3. Defer + conformance retry (resolution/index.ts): a method the property's type inherits from a supertype is resolvable only once implements/extends edges exist, so refs matching PHP_PROP_SHAPE are pushed to the existing deferred-chain retry and dispatched back through matchMethodCall in the conformance pass — the Tracking: chained factory/singleton call resolution across statically-typed languages #750/TS/JS class fields with type annotations are extracted as method-kind nodes #808 lifecycle, no new wiring.

Validation

  • 15-case corpus: promoted / classic / interface-typed / parent-class / trait properties, inherited method (via the conformance retry), two same-named methods on unrelated classes (edge goes to the property's declared type, not the first-indexed homonym), local variable shadowing a property (Local-variable receiver-type inference is C++-only — instance method calls through a local don't resolve in other languages #1108 path still wins), untyped docblock-only property (deliberately no edge), deep chain $this->a->b->m() (deliberately no edge, parity with other languages).
  • 9 new vitest tests (__tests__/php-property-receiver-resolution.test.ts), modeled on the CFML receiver-inference tests; full suite passes.
  • Six real PHP codebases (340–9 200 files, PHP 7.1–8.5, Symfony 3.2–8.0): a sampled repository method went 0/7 → 7/7 production callers; 49/49 manually verified property-receiver edges correct across five typed codebases, zero false positives in random samples of confidence-0.9 edges; +6 100 call edges on a 5 000-file Symfony repo (+3.8%). On an untyped symfony-1.x legacy codebase the fix simply doesn't fire (no recoverable types) — no new wrong edges there either.

Known limitations (deliberately out of scope, candidates for follow-ups)

…clared type (colbymchenry#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>
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