Skip to content

fix(resolver): disambiguate qualified cross-file calls by full namespace tail#479

Open
halindrome wants to merge 2 commits into
DeusData:mainfrom
halindrome:fix/cross-file-qualified-call-resolution
Open

fix(resolver): disambiguate qualified cross-file calls by full namespace tail#479
halindrome wants to merge 2 commits into
DeusData:mainfrom
halindrome:fix/cross-file-qualified-call-resolution

Conversation

@halindrome

Copy link
Copy Markdown
Contributor

Closes #478

Problem

A package/namespace-qualified call (e.g. Perl Foo::Bar::run(...)) whose bare symbol name is defined in more than one package collapses onto a single package. resolve_name_lookup reduces the callee to its bare simple name, and when several candidates share that name the namespace-proximity scorer (best_by_import_distance) deterministically picks one winner — so every *::run caller routes to the same definition and the others are orphaned (in_degree 0, looking like dead code).

This is most visible for Perl, which has no cross-file LSP resolver and depends entirely on this generic registry chain, but the defect applies to any ::/.-qualified callee with a non-unique bare name.

Fix

Add qualified_suffix_match: when a callee is qualified and its bare name has multiple candidates, disambiguate by matching the callee's full qualified tail (normalizing ::.) against each candidate QN at a segment boundary. If exactly one candidate matches, resolve to it with a new high-confidence qualified_suffix strategy; otherwise fall through to the existing bare-name scoring.

Language-agnostic and conservative:

  • Bare callees contain no separator → qualified_suffix_match returns NULL → behavior unchanged.
  • Only engages when there is genuine ambiguity (arr->count > 1) and the qualified tail uniquely identifies one candidate; zero or multiple matches fall through to the prior logic.

Tests

resolve_qualified_disambiguates_same_name (in tests/test_registry.c): a symbol defined in three packages, each fully-qualified call routes to its own package, and a bare call stays ambiguous (no qualified_suffix).

Verification

  • scripts/build.sh clean; scripts/test.sh passes (the one unrelated cli_hook_gate_script_no_predictable_tmp_issue384 failure is pre-existing on main, environment-dependent, and untouched by this change).
  • Validated against a real multi-package Perl project: a method defined in three packages, called fully-qualified from a fourth, previously produced one inbound edge (other two orphaned); now produces three correct edges, with 955 qualified calls resolving via the new strategy.

🤖 Generated with Claude Code

@halindrome

Copy link
Copy Markdown
Contributor Author

QA Round 1 — CLEAN

Reviewer model: claude-opus-4-8 · Contract source: issue #478 · Base: main

Contract Verification

Criterion Status Evidence
Qualified callee with a bare name defined in multiple packages resolves to the package named in the call (no collapse) satisfied qualified_suffix_match (registry.c) normalizes ::. and matches the full qualified tail at a segment boundary; integrated in resolve_name_lookup before best_by_import_distance. Test resolve_qualified_disambiguates_same_name verifies App::Alpha/Beta/Gamma::save each route to their own package (PASS).
Language-agnostic (:: and . separators) satisfied Normalization handles ::; dotted callees flow through unchanged. Correct for Perl/Rust :: and Go/Python/C# . forms.
No regression of bare-name resolution satisfied if (!strchr(dotted, '.')) return NULL; → bare callees bypass the strategy. Test asserts bare save does not use qualified_suffix (PASS).
No regression of single-candidate resolution satisfied New strategy gated on arr->count > 1; count==1 still flows to unique_name.

Findings

  • Contract: 0
  • Regression: 0
  • Pre-existing issues: none in the touched code/call chain

Probes that came back clean:

  • Resolver-chain ordering: import_map / same_module run first but cannot pre-empt the new path for ::-qualified callees (their candidate construction embeds raw ::, which never matches dotted stored QNs). For .-qualified callees a higher-confidence import-map hit can still win first — existing, correct behavior, not a regression.
  • Truncation safety: loop bound w + SKIP_ONE < sizeof(dotted) always leaves room for the NUL; no overflow. Over-long callees truncate to a prefix, and matching is on the QN suffix, so a truncated tail cannot spuriously match. Lone trailing : reads the in-bounds NUL — safe.
  • Ambiguity logic: two matching tails → NULL → fall through; no match → NULL → fall through; segment boundary correctly rejects BarFoo.run matching Foo.run.
  • Builds clean under -Wall -Wextra -Werror; full suite passes (resolve_qualified_disambiguates_same_name PASS). The lone failing cli_hook_gate_script_no_predictable_tmp_issue384 is pre-existing, environment-dependent, and references no registry code.

Advisory (non-blocking) — test-coverage hardening

The code handles these correctly, but the single test does not exercise them, so a future refactor could regress silently:

  • No .-separated callee test (the cross-language dotted form).
  • No explicit fall-through assertions: (a) two candidates whose tails both match → falls through; (b) qualified callee matching no candidate → falls through.

Verdict

CLEAN. Satisfies #478, memory-safe and conservative, compiles under -Werror, passes the suite. Suggestions are additive test hardening only.

@halindrome halindrome marked this pull request as ready for review June 16, 2026 19:48
@halindrome

Copy link
Copy Markdown
Contributor Author

Addressed the QA round 1 advisory test-coverage notes in 66aa971 (test-only, no behavior change):

  • dotted callee form (App.Beta.save) resolves via qualified_suffix
  • qualified callee matching no candidate tail → falls through (not qualified_suffix)
  • new resolve_qualified_ambiguous_tail_falls_through: two candidates sharing the same qualified tail are ambiguous → fall through to bare-name scoring

Full suite green (the lone cli_hook_gate_script_no_predictable_tmp_issue384 failure is pre-existing and unrelated).

shanemccarron-maker and others added 2 commits June 16, 2026 18:11
…ace tail

cbm_registry_resolve reduced a package/namespace-qualified callee
(Foo::Bar::sub) to its bare simple name before the multi-candidate name
lookup. When that bare name was defined in several packages, the bare-name
scorer (best_by_import_distance) picked one winner deterministically and
routed every caller to it — collapsing distinct packages' same-named subs
onto a single definition and orphaning the rest. Impact is heaviest for Perl,
which has no cross-file LSP and relies entirely on this generic resolver, but
the defect applies to any "::"- or "."-qualified callee.

Add qualified_suffix_match: when a callee is qualified and its bare name has
multiple candidates, pick the sole candidate whose qualified tail matches at a
segment boundary (normalizing "::" to "."). Returns the unique match with a new
high-confidence "qualified_suffix" strategy; falls through to the existing
bare-name scoring when zero or several candidates match. Language agnostic —
bare callees contain no separator and are unaffected.

Adds resolve_qualified_disambiguates_same_name covering per-package routing of
a name defined in three packages, plus the bare-call (no-disambiguation) case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Harden resolve_qualified_disambiguates_same_name and add a new test per the
QA round 1 advisory notes, guarding the fall-through paths against silent
regression:
- dotted callee form (App.Beta.save) resolves via qualified_suffix
- qualified callee matching no candidate tail falls through (not qualified_suffix)
- resolve_qualified_ambiguous_tail_falls_through: two candidates sharing the
  same qualified tail are ambiguous → fall through to bare-name scoring

Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
@halindrome halindrome force-pushed the fix/cross-file-qualified-call-resolution branch from 66aa971 to 3795560 Compare June 16, 2026 23:11
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.

Qualified cross-file calls collapse onto one namespace when the bare symbol name exists in multiple packages

2 participants