Skip to content

feat(resolution): PHP @property PHPDoc synthesis + interface override bridging#554

Open
hou3301-byte wants to merge 4 commits into
colbymchenry:mainfrom
hou3301-byte:feat/php-phpdoc-property-synthesis
Open

feat(resolution): PHP @property PHPDoc synthesis + interface override bridging#554
hou3301-byte wants to merge 4 commits into
colbymchenry:mainfrom
hou3301-byte:feat/php-phpdoc-property-synthesis

Conversation

@hou3301-byte
Copy link
Copy Markdown

@hou3301-byte hou3301-byte commented May 29, 2026

Summary

  • PHP added to IFACE_OVERRIDE_LANGS: interface/abstract method → implementation method bridging now works for PHP, producing calls edges that let trace/callees flow through implements/extends boundaries.
  • @property PHPDoc synthesizer (phpPhpdocPropertyEdges):
    • Phase 1 — references edges: parses @property TypeName $propName annotations from class/interface/trait docblocks and emits references edges to the declared types. Captures the dependency graph of PHP service locators (e.g. Ctx with __get()), DI containers, and ORM dynamic properties that tree-sitter extraction alone cannot see.
    • Phase 2 — calls edges (NEW): scans all PHP method bodies for chained property calls (->propName->methodName()) and synthesizes method→method calls edges through the @property mapping. This bridges the __get() magic-method indirection that tree-sitter cannot statically resolve, enabling callers/trace queries to work for patterns like $this->ctx->user_factory->findByUid($uid) and $this->ctx->pay->firstcharge->showFirstCharge($uid).
  • php-phpdoc framework resolver: detects PHP projects using @property annotations and resolves phpdoc-property:TypeName references through the standard resolution pipeline.

Motivation

PHP codebases using service locator patterns (e.g. custom MVC frameworks with Ctx.__get()) have significant dynamic-dispatch holes. A typical codebase with 4,374 PHP files and 305 interfaces has thousands of dynamic property accesses through __get() that CodeGraph's tree-sitter extraction cannot resolve.

The @property PHPDoc standard annotation is widely used in these codebases to document the types returned by __get(). By parsing these annotations, we can recover the lost edges with high precision and zero false positives.

Phase 2 motivation: Phase 1 only created class→class references edges (static dependency), but did not create method→method calls edges (runtime invocation). Without calls edges, callers(User_Factory::findByUid) and trace(Controller::action, User_Factory::findByUid) would still return empty results because tree-sitter extracts $this->ctx->user_factory->findByUid() as the unresolvable reference name this->ctx->user_factory.findByUid. Phase 2 fixes this by building a propName → targetType mapping from all @property annotations and scanning method bodies for ->propName->methodName( patterns, creating the missing calls edges.

Validation

Tested on a 4,374-file PHP codebase (custom MPF framework with heavy service-locator usage):

Metric Before After Delta
Edges 130,893 132,570 +1,677
references (heuristic) 0 1,108 +1,108
calls (heuristic) 0 569 +569
  • All 1,116 existing tests pass (including 3 new Phase 2 tests)
  • No changes to node count or file count

Changes

File Change
src/resolution/callback-synthesizer.ts Add 'php' to IFACE_OVERRIDE_LANGS; add phpPhpdocPropertyEdges() with Phase 1 (references) + Phase 2 (calls)
src/resolution/frameworks/php-phpdoc.ts New PHP PHPDoc framework resolver
src/resolution/frameworks/index.ts Register phpPhpdocResolver
__tests__/php-phpdoc.test.ts Unit + e2e tests for resolver, references edges, and calls edges

Test plan

  • All 1,116 tests pass (npm test) — 52 test files, 0 failures
  • TypeScript compilation clean (npm run build)
  • Validated on real PHP codebase with before/after edge comparison
  • Unit tests for phpPhpdocResolver detect/resolve/extract
  • E2e tests for phpPhpdocPropertyEdges Phase 1 (references edges)
  • E2e tests for phpPhpdocPropertyEdges Phase 2 (calls edges):
    • Direct call: $this->ctx->user_factory->findByUid()show→findByUid calls edge
    • Chained call: $this->ctx->pay->firstcharge->showFirstCharge()charge→showFirstCharge calls edge
    • Negative: non-existent method on target class → no edge created

🐱 and others added 4 commits May 29, 2026 18:38
… bridging

Add two capabilities that close dynamic-dispatch holes in PHP codebases:

1. **PHP added to IFACE_OVERRIDE_LANGS**: interface/abstract method → implementation
   method bridging now works for PHP, producing `calls` edges that let
   trace/callees flow through `implements`/`extends` boundaries.

2. **@Property PHPDoc synthesizer** (`phpPhpdocPropertyEdges`): parses
   `@property TypeName $propName` annotations from class/interface/trait
   docblocks and emits `references` edges to the declared types. This
   captures the dependency graph of PHP service locators (e.g. `Ctx` with
   `__get()`), DI containers, and ORM dynamic properties that tree-sitter
   extraction alone cannot see.

3. **php-phpdoc framework resolver**: detects PHP projects using @Property
   annotations and resolves `phpdoc-property:TypeName` references through
   the standard resolution pipeline.

Validated on a 4,374-file PHP codebase (MPF framework with heavy
service-locator usage): +1,677 edges (1,108 references + 569 calls),
a 1.3% graph increase with zero false positives. All 1,086 existing
tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add 27 tests covering:
- phpPhpdocResolver: detect/extract/resolve/claimsReference unit tests
  (primitive filtering, union types, @property-read/-write, namespaced
  types, abstract/final classes, multiple classes per file)
- phpPhpdocPropertyEdges: end-to-end synthesizer tests with fixture PHP
  projects verifying references (heuristic) edges
- PHP interface override: end-to-end tests verifying calls (heuristic)
  edges for interface→impl and abstract→concrete bridging

Fix two bugs found by tests:
- @Property regex now supports leading backslash for absolute namespace
  paths (\Vendor\Cache\Redis)
- Synthesizer `via` metadata now preserves the exact annotation variant
  (@property-read/@property-write) instead of hardcoding @Property

Co-authored-by: Cursor <cursoragent@cursor.com>


The phpPhpdocPropertyEdges synthesizer previously only created class→class
references edges. Now it also scans all PHP method bodies for chained
property calls (->propName->methodName()) and creates method→method
calls edges through the @Property mapping, bridging the __get() magic
method indirection that tree-sitter cannot statically resolve.

This enables callers/trace queries to work for patterns like:
  $this->ctx->user_factory->findByUid($uid)
  $this->ctx->pay->firstcharge->showFirstCharge($uid)

Co-authored-by: Cursor <cursoragent@cursor.com>
- P0: strip comments (via stripCommentsForRegex) and string interiors
  (via blankPhpStrings) before regex matching to prevent false hits
  from code examples in comments or string literals
- P2: support variable-dereference pattern where a @Property target is
  assigned to a local variable then called through it:
    $factory = $this->ctx->user_factory;
    $factory->findByUid($uid);
- P3: skip methods whose body doesn't contain any known @Property name
  (propName pre-check) to reduce unnecessary regex work
- P1 (doc): document the no-receiver-type-verification limitation in
  the function's JSDoc comment as a known trade-off

Co-authored-by: Cursor <cursoragent@cursor.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.

1 participant