From a6f32d0fba55c288269ef93edb3c759932a344ae Mon Sep 17 00:00:00 2001 From: Harshita Joshi Date: Tue, 28 Jul 2026 23:15:55 -0500 Subject: [PATCH] fix(extract): derive unique names for GoogleTest macros to prevent same-file node collisions (#1266) Signed-off-by: Harshita Joshi --- internal/cbm/extract_defs.c | 67 +++++++++++++++++++++++++++++++++++++ tests/test_extraction.c | 37 ++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 7cc4177ef..6d5729c71 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3196,6 +3196,54 @@ static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) return NULL; } +/* C++/CUDA: true when name is a GoogleTest test-definition macro whose + * invocations parse as function definitions with bare-identifier parameters. + * Multiple such macros in one file all share the extracted name (e.g. "TEST"), + * causing qualified-name collisions and node loss (#1266). */ +static bool is_cpp_test_macro(const char *name) { + return strcmp(name, "TEST") == 0 || strcmp(name, "TEST_F") == 0 || + strcmp(name, "TEST_P") == 0 || strcmp(name, "TYPED_TEST") == 0 || + strcmp(name, "TYPED_TEST_P") == 0; +} + +/* Compose a unique name from a GoogleTest-style macro invocation by appending + * the macro arguments: TEST(Suite, Case) -> "TEST_Suite_Case". + * Returns an arena-allocated string, or NULL when the parameters cannot be + * resolved (caller keeps the original name in that case). */ +static char *resolve_cpp_test_macro_name(CBMArena *a, const char *macro, TSNode node, + const char *source) { + TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters")); + if (ts_node_is_null(params)) { + params = find_c_params(node); + } + if (ts_node_is_null(params)) { + return NULL; + } + + const char *args[2] = {NULL, NULL}; + int count = 0; + uint32_t nc = ts_node_named_child_count(params); + for (uint32_t i = 0; i < nc && count < 2; i++) { + TSNode child = ts_node_named_child(params, i); + if (ts_node_is_null(child)) { + continue; + } + TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type")); + char *text = cbm_node_text(a, ts_node_is_null(type_node) ? child : type_node, source); + if (text && text[0]) { + args[count++] = text; + } + } + + if (count == 2) { + return cbm_arena_sprintf(a, "%s_%s_%s", macro, args[0], args[1]); + } + if (count == 1) { + return cbm_arena_sprintf(a, "%s_%s", macro, args[0]); + } + return NULL; +} + static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; @@ -3217,6 +3265,20 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec return; } + /* C++/CUDA: GoogleTest macros (TEST, TEST_F, TEST_P, ...) parse as + * function definitions whose name resolves to the bare macro identifier. + * Multiple test cases per file collide on qualified name; derive a unique + * name from the macro arguments so each gets its own graph node (#1266). */ + bool is_gtest = false; + if ((ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) && + is_cpp_test_macro(name)) { + char *gtest_name = resolve_cpp_test_macro_name(a, name, node, ctx->source); + if (gtest_name) { + name = gtest_name; + is_gtest = true; + } + } + TSNode func_node = unwrap_template_inner(node, ctx->language); CBMDefinition def; @@ -3344,6 +3406,11 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec def.is_test = rust_def_is_test(def.decorators); } + // C++/CUDA: GoogleTest macros are test functions (#1266). + if (is_gtest) { + def.is_test = true; + } + // Docstring def.docstring = extract_docstring(a, node, ctx->source, ctx->language); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 93a329984..81207301b 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -1285,6 +1285,41 @@ TEST(cpp_function) { PASS(); } +/* #1266: GoogleTest TEST() macros with the same name collapse into a single + * node when multiple tests share a file. Each must mint a distinct Function + * node whose name encodes the suite and case arguments. */ +TEST(cpp_gtest_same_name_collision_issue1266) { + CBMFileResult *r = extract( + "namespace demo { int assembleWidget(int s) { return s * 2; } }\n" + "TEST(WidgetSuite, DoublesSmallSize) { demo::assembleWidget(1); }\n" + "TEST(WidgetSuite, DoublesZero) { demo::assembleWidget(0); }\n" + "TEST(WidgetSuite, DoublesLargeSize) {\n" + " demo::assembleWidget(1000);\n" + "}\n", + CBM_LANG_CPP, "t", "direct_test.cpp"); + ASSERT_NOT_NULL(r); + ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesSmallSize")); + ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesZero")); + ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesLargeSize")); + ASSERT(!has_def(r, "Function", "TEST")); + cbm_free_result(r); + PASS(); +} + +/* #1266: TEST_F fixture macro also produces unique names. */ +TEST(cpp_gtest_f_unique_name_issue1266) { + CBMFileResult *r = extract( + "TEST_F(MyFixture, FirstTest) { doStuff(); }\n" + "TEST_F(MyFixture, SecondTest) { doOtherStuff(); }\n", + CBM_LANG_CPP, "t", "fixture_test.cpp"); + ASSERT_NOT_NULL(r); + ASSERT(has_def(r, "Function", "TEST_F_MyFixture_FirstTest")); + ASSERT(has_def(r, "Function", "TEST_F_MyFixture_SecondTest")); + ASSERT(!has_def(r, "Function", "TEST_F")); + cbm_free_result(r); + PASS(); +} + /* --- C++ out-of-line method definitions (#428) --- * A .cpp defining methods of a class declared elsewhere (not in this TU). * Pre-fix these were recorded as free Functions (label "Function", no @@ -4816,6 +4851,8 @@ SUITE(extraction) { RUN_TEST(rust_enum); RUN_TEST(zig_struct); RUN_TEST(cpp_function); + RUN_TEST(cpp_gtest_same_name_collision_issue1266); + RUN_TEST(cpp_gtest_f_unique_name_issue1266); RUN_TEST(cpp_out_of_line_method_issue428); RUN_TEST(cobol_paragraph); RUN_TEST(verilog_module);