Repository navigation
fix(cs-lsp): bind same-name C# types by declared namespace (#2120) - #2347
Conversation
|
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. |
b220ece to
95f0912
Compare
…l-ns types Follow-up to DeusData#2347 (issue DeusData#2120). Step 8b binds same-name TOP-LEVEL types by declared namespace, but two shapes still fell through to the short-name fallback, where the prefix score against a path-derived QN is 0 for every variant and registration order decided: 1. A qualified `A.B` whose qualifier A is a TYPE, not a namespace: `new Command.Result(...)` and `new Grain.Handler(dep).Handle(...)`. cs_visible_namespaces only tried A as a namespace, and nested types carry no namespace_qn, so the scan never had a candidate. Resolution now falls back to resolving the qualifier as a type (recursively, one segment shorter) and binding the nested member through the registry. 2. A top-level type with NO namespace declaration (global namespace), e.g. a service consumed via [Inject]: cs_top_level_namespace returned NULL for it, making it invisible to step 8b even though C# sees the global namespace from everywhere. Top-level C# types now register namespace_qn "" and cs_visible_namespaces appends the global namespace last, per C# lookup order. Test: parallel_csharp_nested_type_and_global_namespace_binding covers a nested-type constructor call and the member call on its result, a chained-constructor receiver `new Grain.Handler(dep).Handle()`, and a same-name type split across the global namespace and an imported namespace — in the sequential pipeline and in the parallel pipeline with the production Tier-2 C# registry. Decoy files are registered first, so the registry-order behavior fails the test without the fix (verified: nested_code=0/1 chained=0/1 global_ns=0/0 without it). Proof on the private repository behind DeusData#2120 (2,735 CALLS edges into same-name class families, adjudicated against source): mis-bound edges 123 -> 109; the 14 fixed are exactly the chained-constructor receivers. The remaining 109 route through other layers, not type-name resolution: ~90 constructor edges whose cs_ctor row loses the pass_calls carrier join to the textual suffix fallback, a few `X x = new(...)` target-typed implicit constructions, and 3 [Inject] field-type resolutions that run in the registration context without file usings. Full suite: 8176 passed, 1 failed, 8 skipped; the failure (tests/test_cli.c:2080) is a CLI/environment test unaffected by this diff (the cli suite also fails to complete in this environment without the patch). Signed-off-by: Ersin Tarhan <ersintarhan@gmail.com>
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>
2986050 to
a510646
Compare
Conflict in internal/cbm/cbm.h resolved by keeping both added CBMDefinition fields: decl_namespace (this branch) and the test-definition role/spans (main). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Conflict in internal/cbm/result_compact.c: main (#2543) added the variants copy and this branch the decl_namespace copy at the same place in cr_walk_def; both kept. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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