From bc44de15d38195de217511ffe52ef2d7e9155a8b Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 1 Nov 2026 01:07:37 +0100 Subject: [PATCH] fix(cs-lsp): bound the nested-type qualifier walk Resolving a qualified type name A.B that no namespace binds now resolves the qualifier A as a type first (nested types). A using alias whose target is qualified by the alias itself re-enters that walk through the alias step without end: `using Loop = Loop.Inner;` resolves Loop to the target Loop.Inner, whose qualifier is Loop again, and a mutual pair (`using Ping = Pong.Inner; using Pong = Ping.Inner;`) cycles the same way. Both are legal C# when namespaces of those names exist, and the walk overflowed the stack on them. The walk now carries a depth counter in the per-file context and stops after CS_QUALIFIER_MAX_DEPTH (8) levels. One level is one type-nesting step: a namespace-qualified top-level type binds without recursing, so real nesting stays far below the bound. Test: cs_lsp::cslsp_cyclic_alias_qualifier_terminates (self and mutual alias cycles; the file must still resolve Svc.Go). RED without the bound: AddressSanitizer stack-overflow, exit 134. GREEN with it. Signed-off-by: Martin Vogel --- internal/cbm/lsp/cs_lsp.c | 12 ++++++++++-- internal/cbm/lsp/cs_lsp.h | 5 +++++ tests/test_cs_lsp.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/cbm/lsp/cs_lsp.c b/internal/cbm/lsp/cs_lsp.c index 4c715661c..978fb5b71 100644 --- a/internal/cbm/lsp/cs_lsp.c +++ b/internal/cbm/lsp/cs_lsp.c @@ -51,6 +51,10 @@ #define CS_USING_INITIAL_CAP 16 #define CS_NAMESPACE_INITIAL_CAP 8 #define CS_LSP_PARENT_WALK_MAX 32 +/* Nested-type qualifier walk depth: one level per type nesting step of `A.B.C` + * (a namespace-qualified top-level type binds without recursing), so 8 covers + * real nesting while a cyclic alias such as `using A = A.B;` stops after 8. */ +#define CS_QUALIFIER_MAX_DEPTH 8 extern const TSLanguage *tree_sitter_c_sharp(void); @@ -431,9 +435,13 @@ static const char *cs_resolve_by_declared_namespace(CSLSPContext *ctx, const cha * 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) { + * `new Grain.Handler(dep).Handle(...)`). The walk is depth-bounded: an + * alias whose target names the alias itself (`using A = A.B;`, or a + * mutual pair) re-enters this path through step 8 with no end. */ + if (!best && qualifier && ctx->qualifier_depth < CS_QUALIFIER_MAX_DEPTH) { + ctx->qualifier_depth++; const char *qtype = cs_resolve_type_name(ctx, qualifier); + ctx->qualifier_depth--; if (qtype) { const char *nested = cbm_arena_sprintf(ctx->arena, "%s.%s", qtype, simple); if (cs_lookup_type_qn(ctx, nested)) diff --git a/internal/cbm/lsp/cs_lsp.h b/internal/cbm/lsp/cs_lsp.h index 27cc233a5..b7b6c4dc8 100644 --- a/internal/cbm/lsp/cs_lsp.h +++ b/internal/cbm/lsp/cs_lsp.h @@ -111,6 +111,11 @@ typedef struct { /* Recursion guard for cs_eval_expr_type. */ int eval_depth; + /* Recursion guard for the nested-type qualifier walk of + * cs_resolve_type_name: resolving `A.B` resolves the qualifier `A` as a + * type first, and a `using A = A.B;` alias cycles back into it. */ + int qualifier_depth; + /* Debug mode (CBM_LSP_DEBUG env). */ bool debug; } CSLSPContext; diff --git a/tests/test_cs_lsp.c b/tests/test_cs_lsp.c index 6c111ca2d..54184ea09 100644 --- a/tests/test_cs_lsp.c +++ b/tests/test_cs_lsp.c @@ -1226,6 +1226,34 @@ TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site) { PASS(); } +/* A qualified type name resolves its qualifier as a TYPE when no namespace + * binds it (nested types, #2120). A using alias whose target is qualified by + * the alias itself (`using Loop = Loop.Inner;`) or by a mutual alias re-enters + * that walk through the alias step forever: each `Loop` resolves the target + * `Loop.Inner`, whose qualifier is `Loop` again. The walk is depth-bounded; + * the file must still finish and resolve its ordinary calls. */ +TEST(cslsp_cyclic_alias_qualifier_terminates) { + const char *src = "using Loop = Loop.Inner;\n" + "using Ping = Pong.Inner;\n" + "using Pong = Ping.Inner;\n" + "namespace App {\n" + " public class Svc { public void Go() { } }\n" + " public class Runner {\n" + " public void Run(Loop a, Ping b) {\n" + " var x = new Loop();\n" + " var y = new Ping.Leaf();\n" + " var s = new Svc();\n" + " s.Go();\n" + " }\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_cs(src); + ASSERT(r); + ASSERT(require_resolved(r, "Runner.Run", "Svc.Go") >= 0); + cbm_free_result(r); + PASS(); +} + /* ── Suite ─────────────────────────────────────────────────── */ SUITE(cs_lsp) { @@ -1290,4 +1318,5 @@ SUITE(cs_lsp) { RUN_TEST(cslsp_stringbuilder_chain); RUN_TEST(cslsp_httpclient_chain); RUN_TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site); + RUN_TEST(cslsp_cyclic_alias_qualifier_terminates); }