Skip to content

PHP: union- and intersection-typed receivers still mint a same-file bare-name edge (user story 11) #9

Description

@filipechagas

Parent

Spec: #1 (upstream: Graphify-Labs#1682). Found in independent code review of feat/php-member-calls-1682.

Problem

Spec #1, user story 11:

As a graph consumer, I want no calls edge when the receiver is untyped, union-typed, or intersection-typed, so that common method names (log(), find(), execute()) never produce false cross-class edges.

Union- and intersection-typed receivers still mint a false cross-class edge when the candidate methods live in the same file as the call.

Mechanism

A union or intersection type is correctly refused by the concrete-type policy, so it stamps no receiver_type. But _php_defer is derived from whether a type was stamped (graphify/extractors/engine.py:4737-4742):

_php_receiver_type: str | None = None
if config.ts_module == "tree_sitter_php":
    if php_inline_new_type:
        _php_receiver_type = php_inline_new_type
    elif member_receiver and member_receiver != "this":
        _php_receiver_type = (receiver_types or {}).get(member_receiver)
_php_defer = bool(_php_receiver_type)

So a refused type is indistinguishable from an untyped one, _php_defer is False, and the legacy in-file bare-name match fires. PHP is additionally exempted from the capitalized-receiver strip that would otherwise catch it (engine.py:4748).

This follows from #2's accepted deviation — "defer only when a receiver type was stamped, preserving untyped same-file edges" — but that wording names only untyped, whereas US 11 groups union and intersection alongside it. The residual is undocumented.

Repro (verified through the extract() seam)

One file, U.php:

<?php
namespace App;
class Alpha { public function run(): int { return 1; } }
class Beta  { public function run(): int { return 2; } }
class Ctrl {
    private Alpha|Beta $svc;
    public function go(): int { return $this->svc->run(); }
}

Result — bound to Beta purely by bare-name match order, at the strongest confidence label:

Ctrl::go -> Beta::run   [EXTRACTED]

private Gamma&Delta $svc; behaves identically.

Pre-existing, not a regression: the identical edge reproduces at the branch's merge-base 4e7e6b1. The branch leaves US 11 unmet rather than breaking it.

The guarding tests must be restructured

The existing negatives structurally cannot catch this, so a fixer must not trust them as coverage:

  • tests/test_php_member_calls.py:211test_union_typed_property_emits_no_edge spreads **_CORPUS, which puts the decoy classes (LeadHunterService, AuditLog) in separate files from the controller. The in-file bare-name arm therefore never runs, and the test passes for the wrong reason. It needs a variant whose decoy sits in the same file as the call.
  • No intersection-typed test exists at all — grep for intersection (or a & type) in tests/test_php_member_calls.py returns nothing, despite US 11 naming intersection explicitly.

What to build

Make a refused PHP receiver type suppress the in-file bare-name match, while genuinely untyped receivers keep it (preserving #2's accepted deviation and user story 9). That likely means distinguishing "no type annotation" from "type annotation present but refused" at the stamping site, rather than collapsing both to _php_defer is False.

Acceptance criteria

  • Union-typed receiver with both candidate classes in the same file yields no calls edge
  • Intersection-typed receiver, same-file candidates, yields no calls edge
  • Genuinely untyped receiver still produces today's same-file edge (PHP member calls tracer bullet: $this->prop->method() resolves to an INFERRED calls edge #2's accepted deviation, user story 9)
  • $this->method() same-file calls unchanged (user story 9)
  • The separate-file union test is joined by a same-file variant; an intersection test is added
  • Full suite green (baseline on feat/php-member-calls-1682: 3988 passed / 36 skipped)

Blocked by

None.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions