From 2ef6ba7101b9b0897cbd48c53254599b8bf43068 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 28 Sep 2026 19:34:08 +0200 Subject: [PATCH] fix(extract): name macro-wrapped C/C++ definitions after the macro argument (#946, #1957) Why: a function-like macro in a definition's declarator, `void CBasePlayer::__API_HOOK(ReloadWeapons)(int slot)` or `static void FNAME(first_caller)(void)`, parses as a function_declarator nested directly inside the definition's own function_declarator. The declarator walk descended to the terminal identifier, which is the MACRO, so every definition wrapped by the same macro got one qualified name. ReGameDLL_CS's hook definitions landed as 20 nodes literally named __API_HOOK, and the Linux kernel's arch/x86/kvm/mmu/paging_tmpl.h (19 FNAME(...) definitions) as a single Function FNAME. The last definition won, calls in every other body were sourced to it with a call line outside its range, and callers never reached the real functions. The C LSP had its own copy of the walk: it named the caller after the macro and bound the macro's argument list as the function's parameters, so member calls through a real parameter went unresolved. Fix: cbm_c_macro_wrapped_name_node() recognises the shape. Valid C/C++ never nests two function_declarators directly (a function cannot return a function; a function-pointer return has a parenthesized_declarator in between), so the nesting always means a macro call. When that macro is a plain or class-qualified name taking exactly one bare-name argument, the definition is named after the argument; any other shape (multi-argument wrappers, non-name arguments) keeps today's walk. The shared C-family declarator resolver uses it, so the def name, the call-scope QN and the unified extractor agree; the C LSP uses it too and keeps the outer (real) parameter list. The class scope of `Cls::MACRO(x)` was already derived from the qualified name and is unchanged. Proof: dev builds of origin/main 1f9b4db1 (before) and this change (after), same machine, fresh indexes. rehlds/ReGameDLL_CS @ 4a50c42 (#946): - nodes named __API_HOOK: 20 -> 0 - Function 986 -> 1003, Method 3768 -> 3859: 129 hook definitions now have their own node, and every added definition starts on a `__API_HOOK()(` line (0 false definitions). Removed: the 20 __API_HOOK nodes, plus CGrenade::Explode(Vector, Vector), which now shares CGrenade.Explode with its hooked overload, as any C++ overload pair does today (one node per QN). - CALLS 9436 -> 9916; CALLS sourced from __API_HOOK 410 -> 0; callable- sourced CALLS whose call line lies outside the caller's range 565 -> 198. - parse_partial files 131 -> 131. - 5 samples checked against source (CBasePlayer::Spawn 5771-6142 with 32 in-range out-calls incl. ReloadWeapons via lsp_implicit_this, CBasePlayer::TakeDamage, PM_Move, CGrenade::ShootSmokeGrenade, CBotManager::OnEvent). - 18 of the 147 hook definitions are still not extracted, before and after: the parser loses them to raw ERROR regions (a `T *EXT_FUNC` pointer return misparsed as a multiplication, a preceding semicolon-less LINK_HOOK_*(...) invocation, an #ifdef-split signature). That is parser recovery, not naming, and is left for a follow-up. Linux arch/x86/kvm/mmu @ 2d2338c93 (#1957): - paging_tmpl.h: 1 Function FNAME -> 19 distinct functions (all 19 FNAME(...) definitions; 0 false definitions in the directory). - CALLS from paging_tmpl.h functions with the call line outside the caller's range: 32 -> 0 (directory: 35 -> 3, the 3 pre-existing in mmu.c); e.g. fetch 651-786 now owns its 13 calls, sync_spte 934-1005 its 8. - 5 samples checked against source (prefetch_gpte, walk_addr_generic, fetch, sync_spte, page_fault). - #1957's own reproduction: first_caller and later_function are distinct Functions, sink() at line 9 is sourced to first_caller (7-10), and main -> first_caller resolves. Tests: extraction::extract_cpp_macro_wrapped_declarator_names_def_issue946 (qualified + unqualified + EXT_FUNC hooks, macro in a header: def names, QNs, range, real parameter count, per-body call scope), extraction::extract_c_fname_macro_definitions_stay_distinct_issue1957 (the reported C reproduction incl. the LSP caller QN, and the kernel template shape with a token-pasting FNAME defined only under #if), extraction::extract_c_macro_wrapped_declarator_controls_issue946 (precision controls: file-scope and in-body macro call statements, a call through a returned function pointer, a real function returning a function pointer, a macro-wrapped prototype and a two-argument wrapper mint no definition named after a macro argument), and c_lsp::clsp_macro_wrapped_declarator_caller_qn_issue946 (LSP caller QN, lsp_implicit_this, typed parameter). RED 3x before, GREEN after, RED on revert; the LSP half alone binds the c_lsp test and the #1957 test. Fixes #946 Fixes #1957 Signed-off-by: Martin Vogel --- internal/cbm/helpers.c | 42 +++++++++ internal/cbm/helpers.h | 12 +++ internal/cbm/lsp/c_lsp.c | 25 ++++++ tests/test_c_lsp.c | 37 ++++++++ tests/test_extraction.c | 190 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+) diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 1b77f0bdc..c5b81000b 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -1065,6 +1065,42 @@ TSNode cbm_c_recovered_func_name(TSNode func_declarator) { return last; } +// Macro-wrapped definition name — see the header for the full rationale (#946). +TSNode cbm_c_macro_wrapped_name_node(TSNode decl) { + TSNode null_node = {0}; + if (ts_node_is_null(decl) || strcmp(ts_node_type(decl), "function_declarator") != 0) { + return null_node; + } + TSNode macro = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); + if (ts_node_is_null(macro) || strcmp(ts_node_type(macro), "function_declarator") != 0) { + return null_node; + } + // The macro itself is a plain or class-qualified name: FNAME / Cls::__API_HOOK. + TSNode macro_name = ts_node_child_by_field_name(macro, TS_FIELD("declarator")); + const char *mk = ts_node_is_null(macro_name) ? "" : ts_node_type(macro_name); + if (strcmp(mk, "identifier") != 0 && strcmp(mk, "qualified_identifier") != 0) { + return null_node; + } + // Exactly one argument, and that argument is a bare name: the grammar reads + // it as a parameter whose type is the name, with no declarator or qualifier. + // A multi-argument wrapper does not say which argument names the function. + TSNode args = ts_node_child_by_field_name(macro, TS_FIELD("parameters")); + if (ts_node_is_null(args) || ts_node_named_child_count(args) != SKIP_ONE) { + return null_node; + } + TSNode arg = ts_node_named_child(args, 0); + if (strcmp(ts_node_type(arg), "parameter_declaration") != 0 || + ts_node_named_child_count(arg) != SKIP_ONE) { + return null_node; + } + TSNode name = ts_node_child_by_field_name(arg, TS_FIELD("type")); + const char *nk = ts_node_is_null(name) ? "" : ts_node_type(name); + if (strcmp(nk, "type_identifier") != 0 && strcmp(nk, "identifier") != 0) { + return null_node; + } + return name; +} + // Resolve function name from C/C++/CUDA/GLSL declarator chain. Shared canonical // implementation — see the header for the full rationale (#438). TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) { @@ -1093,6 +1129,12 @@ TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) { } return resolve_qualified_name(decl); } + /* #946: `FNAME(first)(void)` names the def `first`, not `FNAME` — every + * definition wrapped by the same macro otherwise shares one QN. */ + TSNode wrapped = cbm_c_macro_wrapped_name_node(decl); + if (!ts_node_is_null(wrapped)) { + return wrapped; + } TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); if (ts_node_is_null(inner) && ts_node_named_child_count(decl) > 0) { inner = ts_node_named_child(decl, 0); diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index e74bc6a85..f96f46709 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -89,6 +89,18 @@ bool cbm_c_reserved_func_name(const char *name); // lets one of these files define a name more than once. bool cbm_is_c_preprocessor_lang(CBMLanguage lang); +// Macro-wrapped definition name (#946): `void FNAME(first)(void)` and +// `void Cls::__API_HOOK(Spawn)()` parse as a function_declarator whose own +// declarator is ANOTHER function_declarator — the macro call. Valid C/C++ never +// nests them directly (a function cannot return a function; a function-pointer +// return has a parenthesized_declarator in between), so the shape is always a +// function-like macro spelling the name. When `decl` is such an outer +// function_declarator and the macro takes exactly one bare name, returns that +// name node ("first", "Spawn"); otherwise a null node, and callers keep their +// ordinary declarator walk. Shared by the declarator resolver and the C LSP so +// the def name and the LSP caller QN agree. +TSNode cbm_c_macro_wrapped_name_node(TSNode decl); + // Convert a resolved function/method name node to its name string, normalizing a // C++ conversion-operator's `operator_cast` node (which spans the full // "operator bool() const") down to "operator bool". Shared by the defs and diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 4c78caf8e..a59f36a8f 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -5334,6 +5334,22 @@ static bool c_process_configured_test_body(CLSPContext *ctx, TSNode scope) { return true; } +// #946: a macro-wrapped definition (`Cls::__API_HOOK(Spawn)`, `FNAME(first)`) is +// named by the macro's argument, keeping any class qualifier: replace the last +// `::` segment of the walked name (or the whole unqualified name) with it, the +// same name cbm_resolve_c_declarator_name_node gives the def. +static char *c_macro_wrapped_func_name(CLSPContext *ctx, char *func_name, TSNode macro_arg) { + char *arg = c_node_text(ctx, macro_arg); + if (!func_name || !arg || !arg[0]) + return func_name; + const char *last_sep = NULL; + for (const char *p = strstr(func_name, "::"); p; p = strstr(p + 2, "::")) + last_sep = p; + if (!last_sep) + return arg; + return cbm_arena_sprintf(ctx->arena, "%.*s::%s", (int)(last_sep - func_name), func_name, arg); +} + static void c_process_function(CLSPContext *ctx, TSNode func_node) { if (c_process_configured_test_body(ctx, func_node)) return; @@ -5346,6 +5362,7 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { const char *saved_class_qn = ctx->enclosing_class_qn; const char *saved_func_qn = ctx->enclosing_func_qn; TSNode params_node = (TSNode){0}; + TSNode macro_arg = (TSNode){0}; // Navigate declarator to find name and parameters TSNode cur = decl; @@ -5366,6 +5383,12 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { func_name = c_node_text(ctx, real); break; } + /* #946: the nested declarator is a macro call. These parameters are + * the real ones; step past the macro's own argument list to the + * (possibly class-qualified) macro name, which still scopes it. */ + macro_arg = cbm_c_macro_wrapped_name_node(cur); + if (!ts_node_is_null(macro_arg)) + fdecl = ts_node_child_by_field_name(fdecl, "declarator", 10); cur = fdecl; continue; } @@ -5425,6 +5448,8 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { } break; } + if (!ts_node_is_null(macro_arg)) + func_name = c_macro_wrapped_func_name(ctx, func_name, macro_arg); // A keyword "name" is an error-recovery artifact (see cbm_c_reserved_func_name): // the def extractor mints no node for it, so no caller QN may name it either. diff --git a/tests/test_c_lsp.c b/tests/test_c_lsp.c index d89091150..d243b5c2a 100644 --- a/tests/test_c_lsp.c +++ b/tests/test_c_lsp.c @@ -1168,6 +1168,42 @@ TEST(clsp_macro_wrapped_call) { PASS(); } +/* #946: `Cls::__API_HOOK(Name)(params)` — the C LSP walked the macro-wrapped + * declarator to the macro, so every hook's caller QN was Cls.__API_HOOK (no such + * definition exists once the def walk names the hook), and it bound the MACRO's + * argument list as the function's parameters, so `pOther` was untyped and the + * member call through it went unresolved. */ +TEST(clsp_macro_wrapped_declarator_caller_qn_issue946) { + CBMFileResult *r = extract_cpp("void helper();\n" + "class CBaseEntity {\n" + "public:\n" + " void Touch();\n" + "};\n" + "class CBasePlayer {\n" + "public:\n" + " void ReloadWeapons(int slot);\n" + " void Spawn();\n" + " void Use(CBaseEntity *pOther);\n" + "};\n" + "void CBasePlayer::__API_HOOK(ReloadWeapons)(int slot) {\n" + " helper();\n" + "}\n" + "void CBasePlayer::__API_HOOK(Spawn)() {\n" + " ReloadWeapons(0);\n" + "}\n" + "void CBasePlayer::__API_HOOK(Use)(CBaseEntity *pOther) {\n" + " pOther->Touch();\n" + "}\n" + "void helper() {}\n"); + ASSERT_NOT_NULL(r); + ASSERT_EQ(count_resolved(r, "__API_HOOK", ""), 0); + ASSERT_GTE(find_resolved(r, "CBasePlayer.ReloadWeapons", "helper"), 0); + ASSERT_GTE(find_resolved(r, "CBasePlayer.Spawn", "CBasePlayer.ReloadWeapons"), 0); + ASSERT_GTE(find_resolved(r, "CBasePlayer.Use", "CBaseEntity.Touch"), 0); + cbm_free_result(r); + PASS(); +} + TEST(clsp_macro_with_args) { CBMFileResult *r = extract_c("\n" "int printf(const char* fmt, ...);\n" @@ -16578,6 +16614,7 @@ SUITE(c_lsp) { RUN_TEST(clsp_dependent_member_access); RUN_TEST(clsp_nocrash_try_catch); RUN_TEST(clsp_macro_wrapped_call); + RUN_TEST(clsp_macro_wrapped_declarator_caller_qn_issue946); RUN_TEST(clsp_macro_with_args); RUN_TEST(clsp_recursive_macro); RUN_TEST(clsp_conditional_macro); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 4c62bddc7..6f6d3a2e3 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -8115,6 +8115,193 @@ TEST(extract_cpp_export_macro_collector_raw_string_unterminated_issue1989) { PASS(); } +/* The call to `callee` on source line `line`, or NULL. */ +static const CBMCall *call_at_line(CBMFileResult *r, const char *callee, int line) { + for (int i = 0; i < r->calls.count; i++) { + const CBMCall *c = &r->calls.items[i]; + if (c->start_line == line && c->callee_name && strcmp(c->callee_name, callee) == 0) { + return c; + } + } + return NULL; +} + +/* #946: ReGameDLL_CS defines its hookable methods as + * `void CBasePlayer::__API_HOOK(ReloadWeapons)(...)`, a function-like macro in + * the declarator (`#define __API_HOOK(fname) fname`, from a header). The grammar + * nests that macro call as a function_declarator directly inside the definition's + * own function_declarator, and the declarator walk named the definition after the + * macro: every hook of a class collapsed into one Method `__API_HOOK`, calls in + * each body were sourced to whichever hook was minted last, and no caller of + * ReloadWeapons could reach it. The macro lives in a header here, as it does in + * ReGameDLL, so the preprocessed second pass cannot expand it either. */ +TEST(extract_cpp_macro_wrapped_declarator_names_def_issue946) { + CBMFileResult *r = extract("#include \"precompiled.h\"\n" /* 1 */ + "void helper();\n" /* 2 */ + "class CBasePlayer {\n" /* 3 */ + "public:\n" /* 4 */ + " void ReloadWeapons(int slot);\n" /* 5 */ + " void Spawn();\n" /* 6 */ + "};\n" /* 7 */ + "void CBasePlayer::__API_HOOK(ReloadWeapons)(int slot)\n" /* 8 */ + "{\n" /* 9 */ + " helper();\n" /* 10 */ + "}\n" /* 11 */ + "void EXT_FUNC CBasePlayer::__API_HOOK(Spawn)()\n" /* 12 */ + "{\n" /* 13 */ + " ReloadWeapons(0);\n" /* 14 */ + "}\n" /* 15 */ + "void EXT_FUNC __API_HOOK(PM_Move)(int server)\n" /* 16 */ + "{\n" /* 17 */ + " helper();\n" /* 18 */ + "}\n", /* 19 */ + CBM_LANG_CPP, "p", "player.cpp"); + ASSERT_NOT_NULL(r); + /* One definition per hook, named after the macro argument. */ + ASSERT_EQ(count_defs_named(r, "Method", "__API_HOOK"), 0); + ASSERT_EQ(count_defs_named(r, "Function", "__API_HOOK"), 0); + ASSERT_TRUE(has_def_qn(r, "p.player.CBasePlayer.ReloadWeapons")); + ASSERT_TRUE(has_def_qn(r, "p.player.CBasePlayer.Spawn")); + ASSERT_TRUE(has_def_qn(r, "p.player.PM_Move")); + const CBMDefinition *reload = find_def(r, "ReloadWeapons"); + ASSERT_NOT_NULL(reload); + ASSERT_STR_EQ(reload->label, "Method"); + ASSERT_EQ((int)reload->start_line, 8); + ASSERT_EQ((int)reload->end_line, 11); + /* The real parameter list, not the macro's argument list. */ + ASSERT_EQ(reload->param_count, 1); + /* Each in-body call is sourced to the hook whose body contains it. */ + const CBMCall *c10 = call_at_line(r, "helper", 10); + ASSERT_NOT_NULL(c10); + ASSERT_STR_EQ(c10->enclosing_func_qn, "p.player.CBasePlayer.ReloadWeapons"); + const CBMCall *c14 = call_at_line(r, "ReloadWeapons", 14); + ASSERT_NOT_NULL(c14); + ASSERT_STR_EQ(c14->enclosing_func_qn, "p.player.CBasePlayer.Spawn"); + const CBMCall *c18 = call_at_line(r, "helper", 18); + ASSERT_NOT_NULL(c18); + ASSERT_STR_EQ(c18->enclosing_func_qn, "p.player.PM_Move"); + cbm_free_result(r); + PASS(); +} + +/* #946 precision controls: only a DEFINITION whose declarator nests a one-name + * macro call is renamed. Macro call statements (file scope or in a body), calls + * through a returned function pointer, a real function returning a function + * pointer, a prototype, and a multi-argument wrapper whose name is ambiguous must + * not mint a definition named after a macro argument. */ +TEST(extract_c_macro_wrapped_declarator_controls_issue946) { + CBMFileResult *r = extract("int (*get_handler(int k))(int);\n" /* 1 */ + "int (*pick(int k))(int)\n" /* 2 */ + "{\n" /* 3 */ + " return 0;\n" /* 4 */ + "}\n" /* 5 */ + "REGISTER(on_boot);\n" /* 6 */ + "void WRAP(proto_only)(void);\n" /* 7 */ + "void PAIR(alpha, beta)(void)\n" /* 8 */ + "{\n" /* 9 */ + "}\n" /* 10 */ + "void run(int v)\n" /* 11 */ + "{\n" /* 12 */ + " get_handler(v)(v);\n" /* 13 */ + " TRACE(v_trace);\n" /* 14 */ + "}\n", /* 15 */ + CBM_LANG_C, "p", "controls.c"); + ASSERT_NOT_NULL(r); + ASSERT_TRUE(has_def(r, "Function", "pick")); + ASSERT_TRUE(has_def(r, "Function", "run")); + ASSERT_FALSE(has_def_any(r, "on_boot")); + ASSERT_FALSE(has_def_any(r, "proto_only")); + ASSERT_FALSE(has_def_any(r, "alpha")); + ASSERT_FALSE(has_def_any(r, "beta")); + ASSERT_FALSE(has_def_any(r, "v")); + ASSERT_FALSE(has_def_any(r, "v_trace")); + const CBMCall *handler = call_at_line(r, "get_handler", 13); + ASSERT_NOT_NULL(handler); + ASSERT_STR_EQ(handler->enclosing_func_qn, "p.controls.run"); + cbm_free_result(r); + PASS(); +} + +/* #1957: the same root cause in C. `static void FNAME(first_caller)(void)` and + * `static void FNAME(later_function)(void)` collapsed into one Function FNAME + * (the later one won), and sink() inside first_caller, on line 9, was sourced + * to it: a call site outside its caller's range, from the textual extractor and + * the C LSP alike. The second file is the kernel template-header shape + * (arch/x86/kvm/mmu/paging_tmpl.h): FNAME token-pastes and is defined only + * inside #if branches, so no preprocessing can expand it. */ +TEST(extract_c_fname_macro_definitions_stay_distinct_issue1957) { + CBMFileResult *r = extract("#define FNAME(x) x\n" /* 1 */ + "\n" /* 2 */ + "static void sink(void)\n" /* 3 */ + "{\n" /* 4 */ + "}\n" /* 5 */ + "\n" /* 6 */ + "static void FNAME(first_caller)(void)\n" /* 7 */ + "{\n" /* 8 */ + " sink();\n" /* 9 */ + "}\n" /* 10 */ + "\n" /* 11 */ + "static void FNAME(later_function)(void)\n" /* 12 */ + "{\n" /* 13 */ + "}\n" /* 14 */ + "\n" /* 15 */ + "int main(void)\n" /* 16 */ + "{\n" /* 17 */ + " first_caller();\n" /* 18 */ + " return 0;\n" /* 19 */ + "}\n", /* 20 */ + CBM_LANG_C, "p", "repro.c"); + ASSERT_NOT_NULL(r); + ASSERT_EQ(count_defs_named(r, "Function", "FNAME"), 0); + const CBMDefinition *first = find_def(r, "first_caller"); + ASSERT_NOT_NULL(first); + ASSERT_EQ((int)first->start_line, 7); + ASSERT_EQ((int)first->end_line, 10); + const CBMDefinition *later = find_def(r, "later_function"); + ASSERT_NOT_NULL(later); + ASSERT_EQ((int)later->start_line, 12); + const CBMCall *sink = call_at_line(r, "sink", 9); + ASSERT_NOT_NULL(sink); + ASSERT_STR_EQ(sink->enclosing_func_qn, "p.repro.first_caller"); + int lsp_sink = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->callee_qn && strcmp(rc->callee_qn, "p.repro.sink") == 0) { + ASSERT_STR_EQ(rc->caller_qn, "p.repro.first_caller"); + lsp_sink++; + } + } + ASSERT_GTE(lsp_sink, 1); + cbm_free_result(r); + + CBMFileResult *t = extract("#if PTTYPE == 64\n" /* 1 */ + "\t#define FNAME(name) paging##64_##name\n" /* 2 */ + "#else\n" /* 3 */ + "\t#error Invalid PTTYPE value\n" /* 4 */ + "#endif\n" /* 5 */ + "static int FNAME(is_present)(unsigned long pte)\n" /* 6 */ + "{\n" /* 7 */ + "\treturn test_bit(pte);\n" /* 8 */ + "}\n" /* 9 */ + "static int FNAME(walk)(unsigned long pte)\n" /* 10 */ + "{\n" /* 11 */ + "\treturn check_pte(pte);\n" /* 12 */ + "}\n", /* 13 */ + CBM_LANG_C, "p", "paging_tmpl.h"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(count_defs_named(t, "Function", "FNAME"), 0); + ASSERT_TRUE(has_def_qn(t, "p.paging_tmpl.is_present")); + ASSERT_TRUE(has_def_qn(t, "p.paging_tmpl.walk")); + const CBMCall *bit = call_at_line(t, "test_bit", 8); + ASSERT_NOT_NULL(bit); + ASSERT_STR_EQ(bit->enclosing_func_qn, "p.paging_tmpl.is_present"); + const CBMCall *chk = call_at_line(t, "check_pte", 12); + ASSERT_NOT_NULL(chk); + ASSERT_STR_EQ(chk->enclosing_func_qn, "p.paging_tmpl.walk"); + cbm_free_result(t); + PASS(); +} + /* #668: walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a ~160 KB * C-stack frame that overflowed small thread stacks (the reporter's crash was in * the "definitions pass" on a large SQL file), and whose `top < 4096` push guards @@ -10889,6 +11076,9 @@ SUITE(extraction) { RUN_TEST(extract_cpp_export_macro_collector_raw_string_unterminated_issue1989); RUN_TEST(extract_cpp_export_macro_suffix_variants_issue1989); RUN_TEST(extract_cpp_export_macro_explicit_define_priority_issue1989); + RUN_TEST(extract_cpp_macro_wrapped_declarator_names_def_issue946); + RUN_TEST(extract_c_macro_wrapped_declarator_controls_issue946); + RUN_TEST(extract_c_fname_macro_definitions_stay_distinct_issue1957); RUN_TEST(walk_defs_no_truncation_over_4096_issue668); RUN_TEST(extract_rust_test_attr_marks_is_test_issue855); RUN_TEST(extract_c_test_dir_marks_is_test_issue1294);