Prefer the alias for a PHP use import edge's target - #3432
Conversation
An aliased use clause (use GuzzleHttp\Client as HttpClient) always targeted the bare imported name, ignoring the alias entirely. Two files importing the same external class under different local aliases, a real pattern for disambiguating two same named classes from different namespaces, produced two different stub targets for one class, splitting its identity. The cross file resolver repoints a stub through the file's own alias to FQN map, keyed by the alias when the import has one, so a bare name derived stub could never be found under that key and stayed stuck, unresolved. The alias, when present, is also what the rest of the file actually references, so preferring it here keeps this edge's target consistent with those reference edges too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1849 functions depend on the 251 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 561 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 — 1849 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: 1684 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
102 of 259 test file(s) selected (39%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impacttests/test_indirect_call_catch_binding_shadow.py— impacttests/test_indirect_call_external_import_shadow.py— impacttests/test_indirect_call_for_of_binding_shadow.py— impacttests/test_indirect_call_function_expression_shadow.py— impacttests/test_indirect_call_nested_closure_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_inferred_confidence_rubric.py— impacttests/test_inherited_field_receivers.py— impacttests/test_java_member_calls.py— impacttests/test_java_type_resolution.py— impacttests/test_js_callback_calls.py— impacttests/test_js_dynamic_import_affected.py— impact- … and 52 more
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.
_import_phpalways targeted the bare imported class name (the last segment of the qualified name), completely ignoring ause ... as ...alias. Two files importing the same external class under different local aliases — the exact real-world case an alias exists for, disambiguating two same-named classes from different namespaces — produced two different stub targets for one class, splitting its identity in the graph.Root cause traced through two interacting pieces:
_import_php(inextract.py) always used the bare imported name (e.g.Client) as the provisional stub target, regardless of any alias._resolve_php_type_references(inresolution.py) re-points that stub to the correct external FQN stub by looking up the stub's own bare label in a per-file alias -> FQN map, which is keyed by the alias when the import has one. For a plain (non-aliased) import the bare label happens to match that key, so it resolves correctly. For an aliased import the bare label ("Client") never matches the key ("httpclient"), so the lookup misses and the edge stays stuck on the unresolved bare-name stub.Confirmed against the exact repro from the issue:
After the fix, both files'
importsedges (and their constructor-parameterreferencesedges) consistently point at the singleguzzlehttp_clientnode.Also verified:
usewith a mixed aliased/non-aliased clause (use GuzzleHttp\{Client, Exception as GuzzleException};) resolves both parts correctly.App\Models\Session as ModelSessionvsShopify\Auth\Session as ShopifySession) still resolve to two distinct nodes — the fix does not over-collapse.use function/use constclauses are unaffected (pre-existing behavior, out of scope for this issue).Test plan
test_php_aliased_import_edge_shares_target_with_plain_importandtest_php_distinct_aliases_for_same_named_classes_do_not_collapsetotests/test_php_type_resolution.pypytest tests/test_php_type_resolution.pyand fullpytest tests/ -k phppasstest_ollama_retry_cap.py, missingopenaimodule in this environment;test_ts_normalizer_scales_linearly_on_large_files, a flaky wall-clock timing assertion, confirmed to pass in isolation)python -m tools.skillgen --checkpasses🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh