Skip to content

fix(php): refuse a member call whose receiver type the file claims (#21, closes #16) - #32

Merged
filipechagas merged 1 commit into
v8from
feat/php-name-resolver-21
Aug 5, 2026
Merged

fix(php): refuse a member call whose receiver type the file claims (#21, closes #16)#32
filipechagas merged 1 commit into
v8from
feat/php-name-resolver-21

Conversation

@filipechagas

Copy link
Copy Markdown
Collaborator

Fixes #21. Fixes #16 (spec #18, ticket 3/5 — design risk 6, measured during 0.9.34 release verification).

What

A new PhpNameResolver (graphify/extractors/php.py, mirroring CsharpNameResolver) returns a (node_id, decisive) verdict and is consulted in _resolve_php_member_calls in front of the corpus-wide type_def_nids fallback, in the exact shape of the C# call site. A short receiver-type name claimed by the calling file — a use alias (#16's case) or a written qualified annotation (#20's field) — that does not resolve to the in-corpus definition now refuses instead of falling back to the unique-bare-name match.

Strictly subtractive by construction: positive verdicts can only be the fallback's own answer (the resolver looks up the written short name in the same index), so the change can only delete edges. Positive alias binding stays #22.

Verification beyond the suite

Differential extract against v8 @ e713e02 over a 34-file corpus covering every receiver-typing shape (bare/aliased/group use, use function/const, absolute and relative written FQNs, promoted params, params, new-locals, inline new, first-class callables, interfaces, duplicate short names, non-PSR-4 layout, global namespace): 3 edges deleted, 0 added, 0 re-pointed, 0 confidence changes — the three deleted are exactly the #16 false-edge class (use Vendor\Sdk\Client;, \Vendor\Sdk\Client annotation, relative Local\Client).

A real full-vs-incremental divergence found by probe during development (stripped composer prefixes: App\Weird\Odd at src/Odd.php) was fixed by treating too-short paths as absent evidence, pinned by test_non_psr4_layout_keeps_its_edge_incrementally. The residual gap (declared namespace disagreeing with an alignable path on incremental runs) is #23's marker, recorded in the issue comments.

Test evidence

  • Red first: 7 of 14 initial tests failed against unfixed code (6 refusals + the incremental refusal); the 7 v8-behavior guards passed as required. A 15th test pins the non-PSR-4 leniency.
  • Green: 15/15 new; 128 across the PHP suites; full suite 4087 passed / 36 skipped / 0 failed (baseline 4072/36 + 15).
  • Independently reproduced by the orchestrator: 4087/36.
  • CHANGELOG bullet with explicit deletion scope; the stale "residual false-positive risk" sentence now points at the fix (Java's identical exposure still named as open).

🤖 Generated with Claude Code

A PHP file that writes `use Vendor\Sdk\Client;` has said which `Client` it
means, but `_resolve_php_member_calls` never read `use` statements: it bound
the receiver's short type name through a corpus-wide index whose only refusal
rule was "more than one candidate", so the lone unrelated `App\Local\Client`
satisfied the single-definition guard and minted an INFERRED 0.8 edge into a
class the file never imported (#16).

`PhpNameResolver` mirrors `CsharpNameResolver`: it answers with a
`(node_id, decisive)` verdict built from the `use` metadata on `imports` edges
(#19), the declared-FQN payload (#14) and the same type-definition index the
fallback uses, and is consulted in FRONT of that fallback exactly like the C#
call site. A claimed name that lands on no in-corpus class refuses instead of
falling back. Written qualified annotations (#20) resolve the same way, absolute
or namespace-relative.

Strictly subtractive by construction: every node the resolver returns is looked
up under the receiver's WRITTEN short name in the very index the fallback
consults, so a positive verdict is always the answer the fallback would have
given, and the only behavior change is the refusal. Binding an alias to a class
its short name does not name is a recall addition and stays with #22. Verified
differentially against v8 over a 34-file corpus of PHP receiver-typing shapes:
3 edges deleted, none added, re-pointed or re-scored.

The refusal needs no new persisted marker — the `use` map belongs to the calling
file, which an incremental rebuild always re-dispatches — and a path SHORTER
than the written name is read as a stripped composer prefix rather than as a
contradiction, so a class off its PSR-4 path keeps its edge on both paths.

This is the fix for #16; the issue stays open for the orchestrator's gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant