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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 01:07:37 +01:00
1 parent c0a8ed8b93
commit bc44de15d3
3 files changed
+44 -2

No files matched your search

+10 -2
View File
@@ -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))
+5
View File
@@ -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;
+29
View File
@@ -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);
}