diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index fb5e446a5..d0cf21157 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -246,6 +246,10 @@ typedef struct { * HTTP_CALLS edge to base + path. Tail fields: zero-init stays valid. */ const char *http_client; const char *http_base_url; + /* C# only: declared namespace of a TOP-LEVEL type (NULL for nested types, + * the global namespace and every other language). Per type, because the + * file-level namespace_name records only a file's first namespace. */ + const char *decl_namespace; } CBMDefinition; /* Argument captured from a call expression */ diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index bb56e2870..e0dcae7ed 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -4584,6 +4584,58 @@ static bool extract_sql_ddl_class_def(CBMExtractCtx *ctx, TSNode node, const cha return true; } +/* Name of a C# namespace declaration node (block or file-scoped), or NULL. */ +static const char *cs_namespace_decl_name(CBMArena *a, TSNode ns, const char *source) { + TSNode nm = ts_node_child_by_field_name(ns, TS_FIELD("name")); + if (ts_node_is_null(nm)) { + return NULL; + } + const char *text = cbm_node_text(a, nm, source); + return text && text[0] ? text : NULL; +} + +/* Declared namespace of a C# type declaration: every enclosing namespace + * block (outer to inner) under an optional file-scoped namespace, which the + * grammar may attach either as an ancestor or as a preceding sibling of the + * compilation unit's members. NULL = global namespace. The file-level + * namespace_name keeps only the FIRST namespace of a file, which mislabels + * every type of a multi-namespace file (#2120). */ +static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *source) { + const char *acc = NULL; + bool file_scoped = false; + TSNode top = node; + for (TSNode cur = ts_node_parent(node); !ts_node_is_null(cur); cur = ts_node_parent(cur)) { + const char *k = ts_node_type(cur); + bool is_file_scoped = strcmp(k, "file_scoped_namespace_declaration") == 0; + if (is_file_scoped || strcmp(k, "namespace_declaration") == 0) { + const char *name = cs_namespace_decl_name(a, cur, source); + if (name) { + acc = acc ? cbm_arena_sprintf(a, "%s.%s", name, acc) : name; + } + file_scoped = file_scoped || is_file_scoped; + } + top = cur; + } + if (file_scoped) { + return acc; + } + uint32_t start = ts_node_start_byte(node); + uint32_t n = ts_node_named_child_count(top); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(top, i); + if (ts_node_start_byte(c) >= start) { + break; + } + if (strcmp(ts_node_type(c), "file_scoped_namespace_declaration") == 0) { + const char *name = cs_namespace_decl_name(a, c, source); + if (name) { + return acc ? cbm_arena_sprintf(a, "%s.%s", name, acc) : name; + } + } + } + return acc; +} + static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; const char *kind = ts_node_type(node); @@ -4951,6 +5003,9 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); + if (ctx->language == CBM_LANG_CSHARP && !ctx->enclosing_class_qn) { + def.decl_namespace = cs_type_decl_namespace(a, node, ctx->source); + } def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); def.docstring = extract_docstring(a, node, ctx->source, ctx->language); diff --git a/internal/cbm/lsp/cs_lsp.c b/internal/cbm/lsp/cs_lsp.c index fc3b548ef..23b6c39b5 100644 --- a/internal/cbm/lsp/cs_lsp.c +++ b/internal/cbm/lsp/cs_lsp.c @@ -74,6 +74,8 @@ static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node); static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t); static const CBMType *cs_unwrap_nullable(const CBMType *t); static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node, TSNode *callable_leaf); +static const char *cs_top_level_namespace(const char *type_qn, const char *short_name, + const char *module_qn, const char *namespace_name); /* ── small helpers ──────────────────────────────────────────────── */ @@ -356,6 +358,91 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) { return NULL; } +/* Namespaces through which `qualifier` (the part of a type reference before + * its simple name, NULL for a simple name) can reach a top-level type, in C# + * lookup order: each enclosing namespace innermost outward, then the global + * namespace (qualified references only) or the file's `using` namespaces + * (simple names only -- a using-namespace directive does not import nested + * namespaces). Returns the count written to `out` (at most `cap`). */ +static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const char **out, + int cap) { + int n = 0; + const char *ns = ctx->namespace_count > 0 ? cs_namespace_qn(ctx) : NULL; + while (ns && *ns && n < cap) { + out[n++] = qualifier ? cbm_arena_sprintf(ctx->arena, "%s.%s", ns, qualifier) : ns; + const char *dot = strrchr(ns, '.'); + ns = dot ? cbm_arena_strndup(ctx->arena, ns, (size_t)(dot - ns)) : NULL; + } + if (qualifier) { + if (n < cap) + out[n++] = qualifier; + return n; + } + for (int i = 0; i < ctx->using_count && n < cap; i++) { + const CBMCSUsing *u = &ctx->usings[i]; + if (u->kind == CBM_CS_USING_NAMESPACE && u->target_qn && u->target_qn[0]) + out[n++] = u->target_qn; + } + /* The global namespace is checked last: an unqualified name binds to a + * top-level type declared without a namespace whatever the file imports + * (#2120 residual — such types used to be invisible to step 8b). */ + if (n < cap) + out[n++] = ""; + return n; +} + +/* Bind a (possibly namespace-qualified) type reference to the project type + * whose DECLARED namespace the current file can see, preferring the earliest + * namespace in C# lookup order; equal ranks keep registration order. NULL when + * no top-level type with that simple name is visible (#2120). */ +static const char *cs_resolve_by_declared_namespace(CSLSPContext *ctx, const char *name) { + if (!ctx->registry || !name || !*name) + return NULL; + const char *simple = cs_short_name(name); + if (!simple || !*simple) + return NULL; + const char *qualifier = + simple > name ? cbm_arena_strndup(ctx->arena, name, (size_t)(simple - name - 1)) : NULL; + enum { CS_VISIBLE_NS_MAX = 128 }; + const char *visible[CS_VISIBLE_NS_MAX]; + int visible_count = cs_visible_namespaces(ctx, qualifier, visible, CS_VISIBLE_NS_MAX); + if (visible_count == 0) + return NULL; + + const CBMRegisteredType *best = NULL; + int best_rank = visible_count; + CBMTypeShortIter it; + cbm_registry_types_by_short_name_chain(ctx->registry, simple, &it); + int i; + while ((i = cbm_type_short_iter_next(&it)) >= 0) { + const CBMRegisteredType *cand = &it.reg->types[i]; + if (!cand->namespace_qn || !cand->short_name || strcmp(cand->short_name, simple) != 0) + continue; + for (int r = 0; r < best_rank; r++) { + if (strcmp(cand->namespace_qn, visible[r]) == 0) { + best = cand; + best_rank = r; + break; + } + } + } + /* No namespace-qualified hit: a qualified `A.B` may name a NESTED type — + * A a type in scope (resolved recursively, one segment shorter), B a + * member type of it. Nested QNs are the parent QN plus the simple name, + * which step 8b's namespace scan can never produce because nested types + * carry no namespace_qn (#2120 residual: `new Command.Result(...)`, + * `new Grain.Handler(dep).Handle(...)`). */ + if (!best && qualifier) { + const char *qtype = cs_resolve_type_name(ctx, qualifier); + if (qtype) { + const char *nested = cbm_arena_sprintf(ctx->arena, "%s.%s", qtype, simple); + if (cs_lookup_type_qn(ctx, nested)) + return nested; + } + } + return best ? best->qualified_name : NULL; +} + /* Returns a fully-qualified type name resolved against: * 1. predefined alias (int -> System.Int32) * 2. type-parameter substitution map @@ -365,7 +452,9 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) { * 6. module-prefixed (file-local QN that the unified extractor produced) * 7. each `using namespace X` prefix * 8. each `using A = X` alias substitution - * 9. fall back to bare name (registry will fail; pipeline drops the call) + * 8b. declared namespace of a top-level project type (enclosing namespaces, + * then `using` namespaces) + * 9. short-name fallback, then the bare name (registry miss drops the call) */ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { if (!raw || !*raw) @@ -457,6 +546,11 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { char *target_bare = cs_strip_generic_args(ctx->arena, u->target_qn); if (target_bare && cs_lookup_type_qn(ctx, target_bare)) return target_bare; + /* A project type is registered under its path-derived QN, so + * `using A = Ns.Type;` binds through the declared namespace. */ + const char *declared = cs_resolve_by_declared_namespace(ctx, target_bare); + if (declared) + return declared; return cbm_arena_strdup(ctx->arena, u->target_qn); } if (bare[alias_len] == '.') { @@ -470,6 +564,15 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { } } + /* 8b. Declared-namespace binding for project types (#2120): their QNs are + * path-derived, so steps 5 and 7 cannot hit them; bind by the namespace + * each top-level type declares, in C# lookup order. */ + { + const char *declared = cs_resolve_by_declared_namespace(ctx, bare); + if (declared) + return declared; + } + /* 9. Short-name fallback: scan the registry for any type whose * short_name equals `bare` and whose QN shares the longest namespace * prefix with the file's namespace stack. This recovers cross-file @@ -525,6 +628,28 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { return bare; } +/* Other top-level declarations of the type `t` names: same simple name, same + * declared namespace, different entry. Registration order; at most `cap`. */ +static int cs_namespace_twins(CSLSPContext *ctx, const CBMRegisteredType *t, + const CBMRegisteredType **out, int cap) { + if (!ctx->registry || !t || !t->namespace_qn || !t->short_name) + return 0; + int n = 0; + CBMTypeShortIter it; + cbm_registry_types_by_short_name_chain(ctx->registry, t->short_name, &it); + int i; + while (n < cap && (i = cbm_type_short_iter_next(&it)) >= 0) { + const CBMRegisteredType *c = &it.reg->types[i]; + if (!c->namespace_qn || !c->short_name || !c->qualified_name || + strcmp(c->short_name, t->short_name) != 0 || + strcmp(c->namespace_qn, t->namespace_qn) != 0 || + strcmp(c->qualified_name, t->qualified_name) == 0) + continue; + out[n++] = c; + } + return n; +} + /* Look up a method on a type, walking inheritance chain. */ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn, const char *method_name) { @@ -535,19 +660,34 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn if (f) return f; - /* Walk inheritance chain (base + interfaces + transitive bases). */ const CBMRegisteredType *t = cs_lookup_type_qn(ctx, type_qn); if (!t) return NULL; + /* The other declarations of this same type -- `partial` pieces in other + * files, reference-assembly copies -- are separate registry entries + * (path-derived QNs) but one C# type: a member declared on any of them is + * a member of this one, and so is a base declared on any of them. */ + const CBMRegisteredType *twins[CS_LSP_PARENT_WALK_MAX] = {0}; + int twin_count = cs_namespace_twins(ctx, t, twins, CS_LSP_PARENT_WALK_MAX); + for (int k = 0; k < twin_count; k++) { + f = cbm_registry_lookup_method(ctx->registry, twins[k]->qualified_name, method_name); + if (f) + return f; + } + + /* Walk inheritance chain (base + interfaces + transitive bases). */ const char *visited[CS_LSP_PARENT_WALK_MAX]; int visited_count = 0; const char *frontier[CS_LSP_PARENT_WALK_MAX]; int frontier_count = 0; - if (t->embedded_types) { - for (int i = 0; t->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; i++) { - frontier[frontier_count++] = t->embedded_types[i]; + for (int k = -1; k < twin_count; k++) { + const CBMRegisteredType *decl = k < 0 ? t : twins[k]; + for (int i = 0; decl->embedded_types && decl->embedded_types[i] && + frontier_count < CS_LSP_PARENT_WALK_MAX; + i++) { + frontier[frontier_count++] = decl->embedded_types[i]; } } @@ -3469,6 +3609,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->name; + rt.namespace_qn = + cs_top_level_namespace(d->qualified_name, d->name, module_qn, d->decl_namespace); rt.is_interface = (strcmp(d->label, "Interface") == 0); if (d->base_classes) { int bc = 0; @@ -3620,6 +3762,24 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, /* ── cross-file entry ───────────────────────────────────────────── */ +/* The type's declared `namespace_name` when `type_qn` is a TOP-LEVEL type of + * the file whose module QN is `module_qn` (top-level C# type QNs are exactly + * module.Name; nested ones are module.Outer.Name and stay NULL, since a nested + * type is not reachable by its simple name through a namespace). */ +static const char *cs_top_level_namespace(const char *type_qn, const char *short_name, + const char *module_qn, const char *namespace_name) { + if (!type_qn || !short_name || !module_qn) + return NULL; + size_t mlen = strlen(module_qn); + if (strncmp(type_qn, module_qn, mlen) != 0 || type_qn[mlen] != '.' || + strcmp(type_qn + mlen + 1, short_name) != 0) + return NULL; + /* Top-level C# type: its declared namespace, "" when it declares none + * (global namespace) so step 8b can bind it — NULL stays reserved for + * nested types, which no namespace can reach by simple name. */ + return (namespace_name && namespace_name[0]) ? namespace_name : ""; +} + /* Register one batch of CBMLSPDef[] into a registry. Shared by the * per-file cross-LSP path and the Tier 2 pre-built registry builder. * Def-driven (no per-file AST mutation) so deterministic per def set. */ @@ -3636,6 +3796,8 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDe memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->short_name; + rt.namespace_qn = cs_top_level_namespace(d->qualified_name, d->short_name, + d->def_module_qn, d->namespace_name); rt.is_interface = d->is_interface || (strcmp(d->label, "Interface") == 0); if (d->embedded_types && *d->embedded_types) { /* Parse "|"-separated list. */ diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index c752350b1..10c154b94 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -64,6 +64,12 @@ typedef struct { const CBMType *index_value_type; // V or NULL // Generic constraints, parallel to type_param_names. NULL or shorter array means "any". const CBMType **type_param_constraints; // NULL-terminated, parallel to type_param_names + + // --- C#-specific (NULL for every other language) --- + // Declared namespace of a TOP-LEVEL project type. Graph QNs are path-derived, + // so this is the only way to bind a short name through `using` directives and + // enclosing namespaces (#2120). NULL for nested types and unknown namespaces. + const char *namespace_qn; } CBMRegisteredType; // Hash-table bucket entry. Chains collisions via next-index list for overload sets. diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index 90c8c6d82..afe1e6073 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -275,6 +275,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) { cr_str(c, &d->impl_trait); cr_str(c, &d->http_client); cr_str(c, &d->http_base_url); + cr_str(c, &d->decl_namespace); } static void cr_walk_call(cr_ctx_t *c, CBMCall *call) { diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 3b74cc1eb..c4219df84 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -409,6 +409,12 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch dst->label = label; dst->def_module_qn = module_qn; dst->namespace_name = namespace_name ? cbm_arena_strdup(arena, namespace_name) : NULL; + /* C# types carry their OWN declared namespace: the file-level one is only + * the file's first namespace, wrong for every later namespace (#2120). */ + if (lang == CBM_LANG_CSHARP && cbm_label_is_type_like(src->label)) { + dst->namespace_name = + src->decl_namespace ? cbm_arena_strdup(arena, src->decl_namespace) : NULL; + } dst->is_interface = (strcmp(label, "Interface") == 0 || strcmp(label, "Protocol") == 0); /* Single return-type string. The per-language registrars split on '|' * for multi-return languages (Go); single-return languages just see one diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index ba41b877c..04b5bbdbf 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -1194,7 +1194,8 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c } str_def_fp += def->fingerprint ? (size_t)def->fingerprint_k * sizeof(uint32_t) : 0; str_def_misc += census_len(def->route_path) + census_len(def->route_method) + - census_len(def->impl_trait) + census_len(def->http_base_url); + census_len(def->impl_trait) + census_len(def->http_base_url) + + census_len(def->decl_namespace); } for (int c = 0; c < r->calls.count; c++) { const CBMCall *call = &r->calls.items[c]; diff --git a/tests/test_parallel.c b/tests/test_parallel.c index 5a332878e..071f77d25 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -304,6 +304,8 @@ static cbm_gbuf_t *run_sequential_with_lsp_cross(const char *project, const char * run_parallel_pipeline does) and records how many results the store parked. */ static bool g_harness_spill = false; static int64_t g_harness_parked = -1; +/* Opt-in: hand cbm_parallel_resolve the production Tier-2 C# registry. */ +static bool g_harness_cs_registry = false; static cbm_gbuf_t *run_parallel_with_extract_opts_and_mutator( const char *project, const char *repo_path, cbm_file_info_t *files, int file_count, @@ -375,9 +377,17 @@ static cbm_gbuf_t *run_parallel_with_extract_opts_and_mutator( CBMModuleDefIndex *module_def_index = all_defs ? cbm_pxc_build_module_def_index(all_defs, def_count) : NULL; + /* C# has no per-file cross path (cbm_pxc_run_one has no C# case): production + * resolves it only through the Tier-2 registry, so a harness run that opts + * in builds that one registry exactly as run_parallel_pipeline does. */ + CBMCrossLspRegistries harness_registries = {0}; + if (g_harness_cs_registry && all_defs) { + harness_registries.cs = cbm_cs_build_cross_registry(&cross_arena, all_defs, def_count); + } cbm_parallel_resolve(&ctx, files, file_count, result_cache, &shared_ids, worker_count, all_defs, def_count, def_modules, module_def_index, - NULL /* cross_registries — tests use per-file path */); + g_harness_cs_registry ? &harness_registries + : NULL /* tests use per-file path */); cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids)); cbm_pxc_free_module_def_index(module_def_index); @@ -2473,6 +2483,386 @@ static kotlin_implicit_probe_t probe_kotlin_implicit_site(const CBMFileResult *r return probe; } +/* #2120: two C# projects declare a class with the same simple name in + * namespaces that share a root (Contoso.Platform.Identity.UserIdentityInfo vs + * Contoso.Platform.BackOffice.Identity.UserIdentityInfo). Graph QNs are + * path-derived, so the namespace-prefix lookups never hit a project type and + * the short-name fallback scored both variants 0 against the file namespace: + * registration order picked the target, ignoring the caller's `using` + * directive. Each caller below can see exactly ONE variant (C# binding rules), + * so exactly one edge per caller is correct whatever the file order: plain + * `using` (both directions), enclosing-namespace proximity, and a + * fully-qualified name. The last case is a reference-assembly shape: one file + * declaring several namespaces, whose file-level namespace is only the FIRST + * one; the decoy is registered first, so a mislabeled namespace falls back to + * it. `partial` pieces of one type are separate registry entries, so a member + * declared on another piece must still bind to that piece. */ +TEST(parallel_csharp_same_name_class_binds_by_namespace) { + static const char platform_src[] = + "namespace Contoso.Platform.Identity;\n" + "public class UserIdentityInfo {\n" + " public static UserIdentityInfo ParseFromClaim(string c)\n" + " => new UserIdentityInfo();\n" + "}\n"; + static const char backoffice_src[] = + "namespace Contoso.Platform.BackOffice.Identity;\n" + "public class UserIdentityInfo {\n" + " public static UserIdentityInfo ParseFromClaim(string c)\n" + " => new UserIdentityInfo();\n" + "}\n"; + static const char bo_handler_src[] = + "using Contoso.Platform.BackOffice.Identity;\n" + "namespace Contoso.Platform.BackOffice.CqrsHandlers;\n" + "public class Handler {\n" + " public string Handle(string claim) {\n" + " var info = UserIdentityInfo.ParseFromClaim(claim);\n" + " return info.ToString();\n" + " }\n" + "}\n"; + static const char pf_handler_src[] = + "using Contoso.Platform.Identity;\n" + "namespace Contoso.Platform.Api;\n" + "public class PlatformHandler {\n" + " public string Handle(string claim) {\n" + " var info = UserIdentityInfo.ParseFromClaim(claim);\n" + " return info.ToString();\n" + " }\n" + "}\n"; + static const char nested_ns_src[] = + "namespace Contoso.Platform.BackOffice.Identity.Claims\n" + "{\n" + " public class ClaimReader {\n" + " public string Read(string claim) {\n" + " var info = UserIdentityInfo.ParseFromClaim(claim);\n" + " return info.ToString();\n" + " }\n" + " }\n" + "}\n"; + static const char fq_src[] = "namespace Contoso.Platform.Reports;\n" + "public class Report {\n" + " public string Build(string claim) {\n" + " var info = Contoso.Platform.BackOffice.Identity" + ".UserIdentityInfo.ParseFromClaim(claim);\n" + " return info.ToString();\n" + " }\n" + "}\n"; + static const char decoy_src[] = "namespace Contoso.Decoy;\n" + "public class Worker {\n" + " public void Start() { }\n" + " public void Beta() { }\n" + "}\n"; + static const char multi_ns_src[] = "namespace Contoso.Win32 { public class SafeHandle { } }\n" + "namespace Contoso.Threading {\n" + " public class Worker { public void Start() { } }\n" + "}\n"; + static const char runner_src[] = "using Contoso.Threading;\n" + "namespace Contoso.App;\n" + "public class Runner {\n" + " public void Run() {\n" + " var w = new Worker();\n" + " w.Start();\n" + " }\n" + "}\n"; + static const char part_a_src[] = "namespace Contoso.Parts;\n" + "public partial class Thing { public void Alpha() { } }\n"; + static const char part_b_src[] = "namespace Contoso.Parts;\n" + "public partial class Thing { public void Beta() { } }\n"; + static const char part_user_src[] = "using Contoso.Parts;\n" + "namespace Contoso.App;\n" + "public class PartUser {\n" + " public void Use() {\n" + " var t = new Thing();\n" + " t.Alpha();\n" + " t.Beta();\n" + " }\n" + "}\n"; + const char *project = "cbm_cs_same_name"; + static const char *rels[] = { + "src/Decoy/Worker.cs", + "src/Contoso.Platform/Identity/UserIdentityInfo.cs", + "src/Contoso.Platform.BackOffice/Identity/UserIdentityInfo.cs", + "src/Contoso.Platform.BackOffice/CqrsHandlers/Handler.cs", + "src/Contoso.Platform/Api/PlatformHandler.cs", + "src/Contoso.Platform.BackOffice/Identity/Claims/ClaimReader.cs", + "src/Contoso.Platform/Reports/Report.cs", + "src/Ref/Threading.cs", + "src/App/Runner.cs", + "src/Parts/Thing.A.cs", + "src/Parts/Thing.B.cs", + "src/App/PartUser.cs", + }; + const char *srcs[] = {decoy_src, platform_src, backoffice_src, bo_handler_src, + pf_handler_src, nested_ns_src, fq_src, multi_ns_src, + runner_src, part_a_src, part_b_src, part_user_src}; + enum { N_FILES = 12, N_CHECKS = 13 }; + + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_same_name_XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) { + FAIL("mkdtemp failed"); + } + char paths[N_FILES][512]; + cbm_file_info_t files[N_FILES] = {0}; + for (int i = 0; i < N_FILES; i++) { + snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]); + if (th_write_file(paths[i], srcs[i]) != 0) { + th_rmtree(tmpdir); + FAIL("failed to write C# same-name fixture"); + } + files[i].path = paths[i]; + files[i].rel_path = (char *)rels[i]; + files[i].language = CBM_LANG_CSHARP; + } + + cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL}; + g_harness_cs_registry = true; + graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2); + g_harness_cs_registry = false; + ASSERT_NOT_NULL(graphs[0]); + ASSERT_NOT_NULL(graphs[1]); + + const char *pf = "cbm_cs_same_name.src.Contoso.Platform.Identity.UserIdentityInfo." + "UserIdentityInfo.ParseFromClaim"; + const char *bo = "cbm_cs_same_name.src.Contoso.Platform.BackOffice.Identity." + "UserIdentityInfo.UserIdentityInfo.ParseFromClaim"; + const char *ref_start = "cbm_cs_same_name.src.Ref.Threading.Worker.Start"; + const char *decoy_start = "cbm_cs_same_name.src.Decoy.Worker.Worker.Start"; + const char *part_alpha = "cbm_cs_same_name.src.Parts.Thing.A.Thing.Alpha"; + const char *part_beta = "cbm_cs_same_name.src.Parts.Thing.B.Thing.Beta"; + const char *decoy_beta = "cbm_cs_same_name.src.Decoy.Worker.Worker.Beta"; + bool ok[2][N_CHECKS]; + bool all = true; + for (int g = 0; g < 2; g++) { + cbm_gbuf_t *gb = graphs[g]; + ok[g][0] = has_edge_from_callable_to_qn(gb, "Handler.Handle", bo, "CALLS"); + ok[g][1] = !has_edge_from_callable_to_qn(gb, "Handler.Handle", pf, "CALLS"); + ok[g][2] = has_edge_from_callable_to_qn(gb, "PlatformHandler.Handle", pf, "CALLS"); + ok[g][3] = !has_edge_from_callable_to_qn(gb, "PlatformHandler.Handle", bo, "CALLS"); + ok[g][4] = has_edge_from_callable_to_qn(gb, "ClaimReader.Read", bo, "CALLS"); + ok[g][5] = !has_edge_from_callable_to_qn(gb, "ClaimReader.Read", pf, "CALLS"); + ok[g][6] = has_edge_from_callable_to_qn(gb, "Report.Build", bo, "CALLS"); + ok[g][7] = !has_edge_from_callable_to_qn(gb, "Report.Build", pf, "CALLS"); + ok[g][8] = has_edge_from_callable_to_qn(gb, "Runner.Run", ref_start, "CALLS"); + ok[g][9] = !has_edge_from_callable_to_qn(gb, "Runner.Run", decoy_start, "CALLS"); + ok[g][10] = has_edge_from_callable_to_qn(gb, "PartUser.Use", part_alpha, "CALLS"); + ok[g][11] = has_edge_from_callable_to_qn(gb, "PartUser.Use", part_beta, "CALLS"); + ok[g][12] = !has_edge_from_callable_to_qn(gb, "PartUser.Use", decoy_beta, "CALLS"); + for (int k = 0; k < N_CHECKS; k++) { + all = all && ok[g][k]; + } + } + if (!all) { + for (int g = 0; g < 2; g++) { + printf(" C# same-name diagnostic %s: using_bo=%d/%d using_pf=%d/%d " + "enclosing_ns=%d/%d fully_qualified=%d/%d multi_ns_file=%d/%d " + "partial=%d/%d/%d\n", + g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3], + ok[g][4], ok[g][5], ok[g][6], ok[g][7], ok[g][8], ok[g][9], ok[g][10], ok[g][11], + ok[g][12]); + } + } + cbm_gbuf_free(graphs[0]); + cbm_gbuf_free(graphs[1]); + th_rmtree(tmpdir); + ASSERT_TRUE(all); + PASS(); +} + +/* #2120 residual, reported on a real repo: step 8b binds only namespace- + * qualified TOP-LEVEL types. A qualified `A.B` whose qualifier A is a TYPE + * (nested-type constructor `new Command.Result(...)`, chained-constructor + * receiver `new Grain.Handler(dep).Handle(...)`) and a top-level type with NO + * namespace declaration (global namespace, e.g. a `[Inject]`-consumed service) + * both fell through to the short-name fallback, where the prefix score against + * a path-derived QN is 0 for every variant — registration order picked the + * target. Decoy files are registered FIRST so the old registry-order behavior + * binds every check to the decoy and the test fails. */ +TEST(parallel_csharp_nested_type_and_global_namespace_binding) { + static const char decoy_src[] = + "namespace Acme.Layouts.Cqrs;\n" + "public class GrainCommand {\n" + " public class Handler {\n" + " public int Handle() => 9;\n" + " }\n" + " public class Result {\n" + " public Result(bool ok) { }\n" + " public int Code() => 9;\n" + " }\n" + "}\n"; + static const char partner_src[] = + "namespace Acme.Partner.Cqrs;\n" + "public class GrainCommand {\n" + " public class Handler {\n" + " private readonly int _v;\n" + " public Handler(int v) { _v = v; }\n" + " public int Handle() => _v;\n" + " }\n" + " public class Result {\n" + " public Result(bool ok) { }\n" + " public int Code() => 1;\n" + " }\n" + "}\n"; + static const char backoffice_src[] = + "namespace Acme.BackOffice.Cqrs;\n" + "public class GrainCommand {\n" + " public class Result {\n" + " public Result(bool ok) { }\n" + " public int Code() => 2;\n" + " }\n" + "}\n"; + static const char nested_user_src[] = + "using Acme.Partner.Cqrs;\n" + "namespace Acme.App;\n" + "public class NestedUser {\n" + " public int Run() {\n" + " var r = new GrainCommand.Result(true);\n" + " return r.Code();\n" + " }\n" + "}\n"; + static const char chained_user_src[] = + "using Acme.Partner.Cqrs;\n" + "namespace Acme.App;\n" + "public class ChainedUser {\n" + " public int Run() {\n" + " return new GrainCommand.Handler(3).Handle();\n" + " }\n" + "}\n"; + static const char other_update_src[] = + "namespace Acme.Other.Web.Services;\n" + "public class UpdateService {\n" + " public string Check() => \"other\";\n" + "}\n"; + /* No namespace declaration: the global namespace. */ + static const char global_update_src[] = + "public class UpdateService {\n" + " public string Check() => \"global\";\n" + "}\n"; + static const char boot_src[] = + "namespace Acme.App;\n" + "public class Boot {\n" + " public string Boot1() {\n" + " var u = new UpdateService();\n" + " return u.Check();\n" + " }\n" + "}\n"; + /* The lookup-order probe: the SAME simple name resolves through an + * imported namespace AND exists in the global namespace. C# binds the + * imported namespace first, so ImportUser.Run2 must hit the Other variant + * and never the global one — proving the global namespace is appended + * last in cs_visible_namespaces, not merged as an equal. */ + static const char import_user_src[] = + "using Acme.Other.Web.Services;\n" + "namespace Acme.App;\n" + "public class ImportUser {\n" + " public string Run2() {\n" + " var u = new UpdateService();\n" + " return u.Check();\n" + " }\n" + "}\n"; + const char *project = "cbm_cs_nested"; + static const char *rels[] = { + "src/Decoy/Layouts.cs", + "src/Decoy/OtherUpdate.cs", + "src/Partner/Cqrs/GrainCommand.cs", + "src/BackOffice/Cqrs/GrainCommand.cs", + "src/Globals/UpdateService.cs", + "src/App/NestedUser.cs", + "src/App/ChainedUser.cs", + "src/App/Boot.cs", + "src/App/ImportUser.cs", + }; + const char *srcs[] = {decoy_src, other_update_src, partner_src, + backoffice_src, global_update_src, nested_user_src, + chained_user_src, boot_src, import_user_src}; + enum { N_FILES = 9, N_CHECKS = 10 }; + + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_nested_XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) { + FAIL("mkdtemp failed"); + } + char paths[N_FILES][512]; + cbm_file_info_t files[N_FILES] = {0}; + for (int i = 0; i < N_FILES; i++) { + snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]); + if (th_write_file(paths[i], srcs[i]) != 0) { + th_rmtree(tmpdir); + FAIL("failed to write C# nested-type fixture"); + } + files[i].path = paths[i]; + files[i].rel_path = (char *)rels[i]; + files[i].language = CBM_LANG_CSHARP; + } + + cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL}; + g_harness_cs_registry = true; + graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2); + g_harness_cs_registry = false; + ASSERT_NOT_NULL(graphs[0]); + ASSERT_NOT_NULL(graphs[1]); + + const char *pf_code = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand." + "Result.Code"; + const char *pf_handle = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand." + "Handler.Handle"; + const char *bo_code = "cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand." + "Result.Code"; + const char *decoy_code = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand." + "Result.Code"; + const char *decoy_handle = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand." + "Handler.Handle"; + const char *global_check = "cbm_cs_nested.src.Globals.UpdateService.UpdateService.Check"; + const char *other_check = "cbm_cs_nested.src.Decoy.OtherUpdate.UpdateService.Check"; + bool ok[2][N_CHECKS]; + bool all = true; + for (int g = 0; g < 2; g++) { + cbm_gbuf_t *gb = graphs[g]; + ok[g][0] = has_edge_from_callable_to_qn(gb, "NestedUser.Run", pf_code, "CALLS"); + ok[g][1] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run", bo_code, "CALLS") && + !has_edge_from_callable_to_qn(gb, "NestedUser.Run", decoy_code, "CALLS"); + /* The ctor EDGE itself joins through the pipeline's textual carrier and + * is out of scope here (see PR notes); what must hold is that the + * nested type RESOLUTION is right, proven by the Code()/Handle() edges + * above, and that no ctor edge binds to a variant the caller cannot + * see: neither the BackOffice nor the registry-first decoy. */ + ok[g][2] = !has_edge_from_callable_to_qn( + gb, "NestedUser.Run", + "cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand.Result", + "CALLS") && + !has_edge_from_callable_to_qn( + gb, "NestedUser.Run", + "cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand." + "Result.Result", + "CALLS"); + ok[g][3] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run", + "cbm_cs_nested.src.Decoy.Layouts.GrainCommand." + "GrainCommand.Result.Result", + "CALLS"); + ok[g][4] = has_edge_from_callable_to_qn(gb, "ChainedUser.Run", pf_handle, "CALLS"); + ok[g][5] = !has_edge_from_callable_to_qn(gb, "ChainedUser.Run", decoy_handle, "CALLS"); + ok[g][6] = has_edge_from_callable_to_qn(gb, "Boot.Boot1", global_check, "CALLS"); + ok[g][7] = !has_edge_from_callable_to_qn(gb, "Boot.Boot1", other_check, "CALLS"); + ok[g][8] = has_edge_from_callable_to_qn(gb, "ImportUser.Run2", other_check, "CALLS"); + ok[g][9] = !has_edge_from_callable_to_qn(gb, "ImportUser.Run2", global_check, "CALLS"); + for (int k = 0; k < N_CHECKS; k++) { + all = all && ok[g][k]; + } + } + if (!all) { + for (int g = 0; g < 2; g++) { + printf(" C# nested/global diagnostic %s: nested_code=%d/%d nested_ctor=%d/%d " + "chained=%d/%d global_ns=%d/%d imported=%d/%d\n", + g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3], + ok[g][4], ok[g][5], ok[g][6], ok[g][7], ok[g][8], ok[g][9]); + } + } + cbm_gbuf_free(graphs[0]); + cbm_gbuf_free(graphs[1]); + th_rmtree(tmpdir); + ASSERT_TRUE(all); + PASS(); +} + /* External Kotlin protocol targets must not borrow a project method merely * because the final Class.method segments agree. Raw assertions prove that the * iterator/destructuring carriers and their stdlib semantic rows really exist; @@ -4590,6 +4980,8 @@ SUITE(parallel) { RUN_TEST(parallel_typescript_module_value_usage_respects_lexical_shadows); RUN_TEST(parallel_typescript_import_namespace_exact_parity); RUN_TEST(parallel_tsx_import_namespace_exact_parity); + RUN_TEST(parallel_csharp_same_name_class_binds_by_namespace); + RUN_TEST(parallel_csharp_nested_type_and_global_namespace_binding); RUN_TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail); RUN_TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph); RUN_TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest);