Summary
When a call is package/namespace-qualified (e.g. Perl Foo::Bar::run(...), or any ::/.-qualified callee) and the bare symbol name is defined in more than one package, the generic call resolver (cbm_registry_resolve in src/pipeline/registry.c) collapses every such call onto a single package, orphaning the others.
This makes trace_path / impact-analysis unreliable for any multiply-defined symbol: callers route to the wrong package, and the losing definitions look like dead code (in_degree 0) when they are not.
Root cause
resolve_name_lookup reduces the callee to its bare simple name (simple_name()), looks it up in the by-name index, and — when several candidates share that name — picks one via best_by_import_distance (namespace-proximity scoring). The package qualifier present in the call expression is discarded before this step, so all of Foo::Bar::run, Foo::Baz::run, Foo::Qux::run resolve to the same winner.
This is most visible for Perl, which has no cross-file LSP resolver and relies entirely on this generic registry chain, but the defect applies to any qualified callee whose bare name is non-unique.
Reproduction (shape)
package Foo::Bar; sub run { ... }
package Foo::Baz; sub run { ... }
# in another file:
Foo::Bar::run(); # both currently resolve to ONE of the two
Foo::Baz::run(); # packages; the other is orphaned
Observed: only one *::run definition receives inbound CALLS edges; the others get zero.
Expected: each fully-qualified call resolves to the package named in the call.
Proposed fix
Before the bare-name scorer collapses multiple candidates, disambiguate a qualified callee by matching its full qualified tail (normalizing :: → .) against each candidate's qualified name at a segment boundary. If exactly one candidate matches, resolve to it; otherwise fall through to existing behavior. Language-agnostic — bare callees contain no separator and are unaffected.
PR to follow.
Summary
When a call is package/namespace-qualified (e.g. Perl
Foo::Bar::run(...), or any::/.-qualified callee) and the bare symbol name is defined in more than one package, the generic call resolver (cbm_registry_resolveinsrc/pipeline/registry.c) collapses every such call onto a single package, orphaning the others.This makes
trace_path/ impact-analysis unreliable for any multiply-defined symbol: callers route to the wrong package, and the losing definitions look like dead code (in_degree 0) when they are not.Root cause
resolve_name_lookupreduces the callee to its bare simple name (simple_name()), looks it up in the by-name index, and — when several candidates share that name — picks one viabest_by_import_distance(namespace-proximity scoring). The package qualifier present in the call expression is discarded before this step, so all ofFoo::Bar::run,Foo::Baz::run,Foo::Qux::runresolve to the same winner.This is most visible for Perl, which has no cross-file LSP resolver and relies entirely on this generic registry chain, but the defect applies to any qualified callee whose bare name is non-unique.
Reproduction (shape)
Observed: only one
*::rundefinition receives inbound CALLS edges; the others get zero.Expected: each fully-qualified call resolves to the package named in the call.
Proposed fix
Before the bare-name scorer collapses multiple candidates, disambiguate a qualified callee by matching its full qualified tail (normalizing
::→.) against each candidate's qualified name at a segment boundary. If exactly one candidate matches, resolve to it; otherwise fall through to existing behavior. Language-agnostic — bare callees contain no separator and are unaffected.PR to follow.