Conversation
Two projects declaring a class with the same simple name in namespaces that share a root (Contoso.Platform.Identity.UserIdentityInfo vs Contoso.Platform.BackOffice.Identity.UserIdentityInfo) had every CALLS edge attached to whichever variant registered first, ignoring the caller's `using` directive, enclosing namespace, fully-qualified name and using-alias. Root cause: C# graph QNs are path-derived, so cs_resolve_type_name's namespace-prefix and using-namespace lookups (steps 5 and 7) never hit a project type. Resolution fell through to the short-name fallback, whose prefix score compares the file namespace against the path QN and is 0 for every candidate, so registry order decided. Fix: - Record the declared namespace of each top-level C# type at extraction (CBMDefinition.decl_namespace, walked from the AST). The file-level namespace_name keeps only a file's FIRST namespace, which mislabels every type of a multi-namespace file such as a reference assembly. pxc_build_lsp_def carries it into the type's CBMLSPDef, and result_compact relocates it. - CBMRegisteredType.namespace_qn (C# only, top-level types only) is set by cs_register_lsp_defs, which both the Tier-2 shared registry and the per-file registry use, so sequential and parallel stay in lockstep. - New resolution step 8b binds a simple or qualified type name to the visible declaration in C# lookup order: enclosing namespaces innermost outward, then `using` namespaces (or the global namespace for a qualified name). A `using A = Ns.Type` alias binds the same way. - cs_lookup_method also searches the type's other same-namespace declarations (`partial` pieces, reference-assembly copies). These are separate registry entries but one C# type. Without this, choosing the namespace-correct piece that lacks the member would lose the edge to a name-only fallback. Proof on dotnet/runtime src/libraries (20,866 .cs files, same machine, both binaries deterministic across two runs): CALLS 649,226 -> 657,564; nodes unchanged (595,714). Most retargeted edges move from a target the caller cannot see to one it can see through an enclosing namespace or `using`. Many more move from non-method targets (enum members such as ExpressionType.Constant) to the real partial-class methods (Expression.Constant). The new edges are unqualified and typed calls into sibling `partial` pieces (ImmutableArray.Create, XmlReader.Create, AsnReader.ReadBoolean). Test: parallel::parallel_csharp_same_name_class_binds_by_namespace covers plain `using` in both directions, the enclosing namespace, a fully-qualified name, a multi-namespace file and a `partial` member, in the sequential pipeline and in the parallel pipeline with the production Tier-2 C# registry. The harness gains an opt-in flag for that registry, because cbm_pxc_run_one has no C# case. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
Tested this branch on the real repository behind #2120 (~53k nodes post-index). Verified against source
One build note, probably unrelated to this PR but worth a look: building |
|
Follow-up from me: after the spot-checks above, I ran a full sweep over all 2,735 CALLS edges into same-name class families in the same repo (~2.1k classes, 137 families), adjudicating each edge against the caller's actual The reported modes are fixed — 1,843 edges visibility-verified correct (incl. the alias/enclosing-namespace cases), and ~700 receiver-typed instance calls ( But the sweep found ~125 residual mis-bound edges in three patterns, each verified against source: 1. Nested-type constructor on a duplicated command name (~90 edges; PlayerCommand 27, Result 21, PartnerCommand 19, UserCommand 15, CurrencyListCommand 2 …) // ns CmdA.Shared.Cqrs.Partner // ns CmdB.Shared.Cqrs.Domain — same repo, same shape
public class PartnerCommand { public class DomainCommand {
public class Result { … } public class Result { public class ResultModel { … } }
} }
// caller imports ONLY CmdA.Shared.Cqrs.Partner:
return new PartnerCommand.Result(false, "x", null); // edge → CmdB…DomainCommand.Result.ResultModelWorst case is cross-project: a BackOffice handler with 2. Chained-constructor receiver dropped (19 edges) return await new GrainOtp.Handler(dataSource, sender).Handle(cmd);
// file imports only the GrainOtp namespace; edge → GrainLayouts.Handler.Handle15 of the 19 all pin to the same unrelated variant, 4 to a second one — same "registry order wins" signature as the original bug, so it looks like the explicit 3. Blazor code-behind Patterns 1–2 look like the remaining paths where the new namespace-aware steps don't kick in (nested-type qualification and constructor-chained receivers). Happy to package a minimal multi-file repro if useful — the three patterns reproduce 100% deterministically on this repo. |
Two projects declaring a class with the same simple name in namespaces
that share a root (Contoso.Platform.Identity.UserIdentityInfo vs
Contoso.Platform.BackOffice.Identity.UserIdentityInfo) had every CALLS
edge attached to whichever variant registered first, ignoring the
caller's
usingdirective, enclosing namespace, fully-qualified name andusing-alias.
Root cause: C# graph QNs are path-derived, so cs_resolve_type_name's
namespace-prefix and using-namespace lookups (steps 5 and 7) never hit a
project type. Resolution fell through to the short-name fallback, whose
prefix score compares the file namespace against the path QN and is 0 for
every candidate, so registry order decided.
Fix:
(CBMDefinition.decl_namespace, walked from the AST). The file-level
namespace_name keeps only a file's FIRST namespace, which mislabels
every type of a multi-namespace file such as a reference assembly.
pxc_build_lsp_def carries it into the type's CBMLSPDef, and
result_compact relocates it.
cs_register_lsp_defs, which both the Tier-2 shared registry and the
per-file registry use, so sequential and parallel stay in lockstep.
visible declaration in C# lookup order: enclosing namespaces innermost
outward, then
usingnamespaces (or the global namespace for aqualified name). A
using A = Ns.Typealias binds the same way.declarations (
partialpieces, reference-assembly copies). These areseparate registry entries but one C# type. Without this, choosing the
namespace-correct piece that lacks the member would lose the edge to a
name-only fallback.
Proof on dotnet/runtime src/libraries (20,866 .cs files, same machine,
both binaries deterministic across two runs): CALLS 649,226 -> 657,564;
nodes unchanged (595,714). Most retargeted edges move from a target the
caller cannot see to one it can see through an enclosing namespace or
using. Many more move from non-method targets (enum members such asExpressionType.Constant) to the real partial-class methods
(Expression.Constant). The new edges are unqualified and typed calls
into sibling
partialpieces (ImmutableArray.Create, XmlReader.Create,AsnReader.ReadBoolean).
Test: parallel::parallel_csharp_same_name_class_binds_by_namespace covers
plain
usingin both directions, the enclosing namespace, afully-qualified name, a multi-namespace file and a
partialmember, inthe sequential pipeline and in the parallel pipeline with the production
Tier-2 C# registry. The harness gains an opt-in flag for that registry,
because cbm_pxc_run_one has no C# case.
Release note: existing indexes need one from-scratch reindex (delete_project + index) to pick this up; a full-mode reindex of unchanged files reuses the old edges.
Fixes #2120