Skip to content

feat(php): positive alias binding via declared FQNs + incremental parity marker (#22, #23) - #35

Merged
filipechagas merged 2 commits into
v8from
feat/php-positive-alias-binding-22-23
Aug 6, 2026
Merged

feat(php): positive alias binding via declared FQNs + incremental parity marker (#22, #23)#35
filipechagas merged 2 commits into
v8from
feat/php-positive-alias-binding-22-23

Conversation

@filipechagas

Copy link
Copy Markdown
Collaborator

Fixes #22. Fixes #23. (Spec #18, tickets 4/5 and 5/5 — shipped together per decision 3: the binding and its carry-over marker must land in the same release.)

What

Two commits, reviewed best in order:

Commit 1 (#22)PhpNameResolver gains a declared-FQN index: casefolded declared FQN -> definition node, built over the same node universe as the short-name census, sourced from the #14 php_class_fqns payload. In resolve_type_name, once the calling file's claim is resolved to an FQN (via use, use ... as, group use, or a qualified annotation), a unique index hit binds before the single-candidate check. Two things now resolve that used to refuse: use App\Alpha\X; selects App\Alpha\X among several same-short-named classes, and a renaming alias use App\Alpha\X as Y; follows the alias to a class the written short name never censuses. An FQN declared identically by two files poisons its entry rather than guessing. On an index miss everything falls through to the #21 rules untouched — additive-only by construction, the mirror risk profile of #21's subtractive-only.

Commit 2 (#23) — the php_class_fqns payload aligns 1:1 with files dispatched this run, so without persistence every #22 edge would exist on full builds only and silently vanish on the first graphify update (the #11 failure mode). Each PHP file node now persists _php_class_fqns into graph.json (the _callable / _php_non_class_types channel), watch.py and cli.py hand it back on resolution-context nodes, and extract() re-injects it as synthetic per-file payload entries keyed by each context node's own source_file. A pre-marker graph.json fails closed: the binding is simply absent — never redirected to a namesake, with the #16 refusals still standing on path evidence — until the defining file is re-extracted.

One existing test changed behavior deliberately: test_duplicate_class_name_emits_no_edge had its caller use App\Services\LeadHunterService; with two namesakes — exactly #22's acceptance scenario 1, which now binds. It was repointed at the still-true refusal (no use claim at all), and test_alias_renaming_to_an_unclaimed_short_name_stays_unresolved (the pinned #21/#22 boundary) now asserts the binding its docstring anticipated.

Verification beyond the suite

Full suite: 4104 passed, 36 skipped (v8 baseline before the change: 4104 collected equivalents green; commit 1 also verified green in isolation, 4097 passed with the #23 files stashed).

Differential extract against v8 @ cec9cdd over a 25-file corpus covering the receiver-typing shapes (namesake pairs with plain/group/renaming use, absolute and namespace-relative written FQNs, bare receiver with no claim, out-of-corpus use, duplicate declared FQN across two files, interface/enum/union refusals, inline new, first-class callable, non-PSR-4 layout, unique-short-name fallback): 6 edges added, 0 deleted, 0 re-pointed, 0 confidence changes. All six target exactly App\Alpha\X::send at INFERRED 0.8 (five calls + one indirect_call through the FCC form); the baseline's 3 edges (inline-new EXTRACTED, non-PSR-4, unique fallback) are byte-identical.

End-to-end incremental parity through the real CLI channel (not the test mirror): graphify extract on that corpus → 9 call edges and 23 file nodes carrying _php_class_fqns in graph.json; touch only the four caller files → graphify update . → the same 9 edges, including every #22 edge whose defining file was left undispatched.

🤖 Generated with Claude Code

filipechagas and others added 2 commits August 6, 2026 10:27
…#22)

The #21 refusal made a claimed name decisive but could only delete
edges. This is its additive counterpart: when the FQN a calling file
claims for a receiver's type — via `use`, `use ... as`, a group `use`,
or a qualified annotation — matches the name some in-corpus file
DECLARES for a class (the #14 payload), PhpNameResolver binds it,
selecting the imported one of several same-short-named classes and
following a renaming alias to a class the written short name would
never census. An FQN declared identically by two files poisons its
index entry rather than guessing; an unclaimed short name still runs
the unique-short-name fallback untouched, and every #16-style refusal
stands where no declared FQN matches.

Ships together with #23 (the persisted `_php_class_fqns` marker):
without it, every edge this adds would exist on full builds only and
silently vanish on the first incremental rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty (#23)

The php_class_fqns payload aligns 1:1 with the files dispatched THIS
run, so every edge the #22 declared-FQN binding adds existed on full
builds only and silently vanished on the first `graphify update` — the
exact failure mode #11 closed for the interface refusal. Stamp each PHP
file node with a persisted `_php_class_fqns` marker (the `_callable` /
`_php_non_class_types` channel), hand it back on the resolution-context
nodes in watch.py and cli.py, and re-inject it as synthetic per-file
payload entries so the resolver rebuilds its index for files the
incremental pass left untouched.

A graph.json written before the marker fails closed, matching the
established precedent: the #22 binding is simply absent — never
redirected to a namesake, with the #16 refusals still standing on path
evidence — until the defining file is re-extracted. Every #22 positive
scenario is mirrored under _full_then_incremental, plus pre-marker
variants pinning the fail-closed behavior.

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 use-import map 5/5: incremental-rebuild parity marker for declared FQNs PHP use-import map 4/5: positive alias binding via namespace index

1 participant