PHP: give use A\B\C as D the imported class's identity (#3421) - #3453
PHP: give use A\B\C as D the imported class's identity (#3421)#3453shivamtiwari3 wants to merge 1 commit into
use A\B\C as D the imported class's identity (#3421)#3453Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes PHP aliased imports so use A\B\C as D mints its import edge on the alias binding D rather than the imported name's last segment, giving it the same target as a plain use A\B\C so _resolve_php_type_references can canonicalize both onto one class node instead of splitting it or leaving the edge dangling. Aliased group-uses (use Ns\{A, Sub\B as C}) now resolve the same way. use function/use const imports are treated as symbol imports and deliberately keep the imported name's last bare segment, since their alias never lands in the resolver's uses map.
Worth a look
- Aliased group-use only mints one import edge for the last clause —
graphify/extract.py:731· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Group-use with aliased clauses handled by unclear child iteration —
graphify/extract.py:731· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
saw_asset globally can attach an alias from one clause to a different clause's name —graphify/extract.py:738· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- PHP group use clauses are not decomposed into their imported members —
graphify/extract.py:738· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Symbol-import (
use function/const) may still consume its alias when structured as a group-use —graphify/extract.py:745· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2070 functions depend on the 460 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 568 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 28 more — each is listed as a finding
Verification — 2070 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1905 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
262 of 262 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/test_csharp_enum_members.py— impact, full-run-safetytests/test_csharp_field_generic_args.py— impact, full-run-safetytests/test_csharp_generic_callsites.py— impact, full-run-safety- … and 212 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_import\_php.
The verifier did not have enough to check \_import\_php, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 36 more finding(s) on lines outside this diff (see the check run).
…#3421) `_import_php` derived the import edge target from the last segment of the *imported* name, discarding both the namespace and the alias. But `_resolve_php_type_references` files each file's imported FQNs in a `uses` map keyed by the **local binding** — the alias when there is one — and canonicalizes an import edge by looking its target up in that map. An aliased `use` therefore minted a target no `uses` entry could match, so the canonicalization silently did nothing. The pass even has a fallback that only makes sense if the target is the alias (`next(alias for alias in uses if _make_id(alias) == tgt)`), which could never fire. Two symptoms follow, both reported: - `use GuzzleHttp\Client` and `use GuzzleHttp\Client as HttpClient` in one project gave `guzzlehttp_client` and `client` — one class, two nodes, neither file reachable from the other's traversal. - Where no bare-name node exists the edge dangles instead: 103 of 145 `dangling_endpoint_edges`, concentrated on exactly the names a project aliases to disambiguate (`session` 44, `client` 21, `connection` 5). The edge is now minted on the local binding, so both forms canonicalize onto the imported class — internal (to its definition node) or external (to an FQN-labeled stub) — and two aliased imports of the same simple name stay distinct instead of collapsing. Aliases inside a group use (`use Ns\{A, Sub\B as C}`) work through the same path. `use function`/`use const` are deliberately excluded: the resolver skips symbol imports, so their alias is absent from `uses` and the imported name's last segment stays the right bare name for the unique-label rewire.
7d0b279 to
4f7419c
Compare
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes PHP use A\B\C as D so its import edge targets the same node as a plain use A\B\C: _import_php now mints the edge on the aliased binding (the name this file's references and _resolve_php_type_references's uses map key on) instead of the imported name's last segment, which had either split the class into a second identity or left the edge dangling (103 of 145 dangling endpoints on the reported repo). Aliased group-uses like use Ns\{A, Sub\B as C} resolve the same way. use function/use const imports are detected as symbol imports and deliberately keep the imported name's bare last segment rather than the alias, since the resolution pass skips them.
Worth a look
- Group-use imports only emit a single edge, dropping all but the first clause —
graphify/extract.py:731· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Symbol import (
use function ... as g) still emits a target derived from the last segment, but binding computation reads only top-level children —graphify/extract.py:733· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2070 functions depend on the 460 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 568 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 28 more — each is listed as a finding
Verification — 2070 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1905 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
262 of 262 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/test_csharp_enum_members.py— impact, full-run-safetytests/test_csharp_field_generic_args.py— impact, full-run-safetytests/test_csharp_generic_callsites.py— impact, full-run-safety- … and 212 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_import\_php.
The verifier did not have enough to check \_import\_php, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 36 more finding(s) on lines outside this diff (see the check run).
Fixes #3421.
Root cause
_import_phpbuilt the import edge target from the last segment of the imported name, discarding both the namespace and the alias:That target is not meant to be final —
_resolve_php_type_referencescanonicalizes it. But that pass keys its per-fileusesmap by the local binding:So for
use GuzzleHttp\Client as HttpClientthe map is{"httpclient": "GuzzleHttp\Client"}while the edge target wasclient— nothing matched, and the canonicalization silently did nothing. The pass even carries a fallback that only makes sense if the target is the alias, and which could therefore never fire:A plain
useworked only because there the binding and the last segment happen to be the same string. The alias is the trigger, exactly as reported.Both reported symptoms, verified
Baseline on the issue's repro:
Baseline on the ambiguous-name shape (the Laravel symptom):
That is the reporter's
session(44 edges) /client(21) /connection(5) breakdown: aliasing exists precisely to disambiguate two classes with one simple name, and the bare name is the one thing that cannot represent either.Fix
Mint the edge on the local binding — the alias when the clause has one, else the imported name's last segment. That is the key the resolver already looks up, so both forms now canonicalize onto the imported class: to its own definition node when internal, to an FQN-labeled stub when external.
After the fix, the same two fixtures:
Group-use aliases (
use Ns\{A, Sub\B as C}) travel the same path, since the resolver already composes the group prefix with each clause.Deliberately out of scope
use function ns\f as ganduse const ns\C as Dkeep the old behavior:_resolve_php_type_referencesskips symbol imports by design (if c.type in ("function", "const"): return # not a class import), so their alias is absent fromusesand switching to it would strand the edge on a one-letter id. The imported name's last segment stays the right bare name for the unique-label rewire. A regression test pins this.Those two still dangle on both sides of the change (
slug,version) — resolving symbol imports needs a function/const index rather than the class index this pass builds, which is a separate change.Tests
Five tests in
tests/test_php_type_resolution.py; the first four fail on unpatchedextract.pyand pass with the fix, the fifth is the function/const regression guard.test_php_aliased_external_import_shares_target_with_plain_import— the issue's repro; also asserts the strayclientnode is gone.test_php_aliased_import_of_internal_class_resolves_to_definitiontest_php_two_aliased_imports_of_same_bare_name_stay_distinct— two targets, neither dangling, internal vs external correctly split.test_php_aliased_group_use_resolvestest_php_aliased_function_and_const_imports_keep_bare_nameFull suite: 5498 passed, 3 pre-existing environment-dependent failures unchanged (
test_extract_code_only_cli, twotest_ollamabackend-detection tests — all fail on a clean tree here too).Note on #3346
The linked TypeScript issue looks like the same shape (identity taken from a name that is not the one the resolver keys on) but a different pass; not addressed here.
🤖 Generated with Claude Code