@@ -7424,6 +7424,71 @@ TEST(extract_c_clean_file_no_recovery_duplicates_issue961) {
74247424 PASS();
74257425}
74267426
7427+ /* True when `qn` carries the token-joined spelling of the `box<int, 2>`
7428+ * template-id that the preprocessed buffer used to produce. */
7429+ static bool qn_has_token_spaced_box(const char *qn) {
7430+ return qn && (strstr(qn, "box <") || strstr(qn, "< int") || strstr(qn, "int ,"));
7431+ }
7432+
7433+ /* dotnet/runtime vxsort shape (src/coreclr/gc/vxsort/smallsort/
7434+ * bitonic_sort.*.generated.h): methods of an explicit specialization
7435+ * `template<> struct bitonic<int64_t, AVX2>` carry an attribute macro defined in
7436+ * another header (INLINE / NOINLINE), so the raw parse has ERROR regions on the
7437+ * method lines and the #961 rescue re-extracts them from the preprocessed
7438+ * buffer. That buffer used to join every token with one space, spelling the
7439+ * scope `box < int , 2 >` where the raw pass spells `box<int, 2>`; the QN dedup
7440+ * (strcmp) missed, so every method got a second node and the CALLS split across
7441+ * both. One definition, one spelling, on every path. */
7442+ TEST(extract_cpp_template_specialization_preprocessed_qn_single_spelling) {
7443+ CBMFileResult *r = extract("#ifndef BOX_H\n"
7444+ "#define BOX_H\n"
7445+ "namespace ns {\n"
7446+ "template <typename T, int M> struct box {\n"
7447+ " static void sort(T *p);\n"
7448+ "};\n"
7449+ "template<> struct box<int, 2> {\n"
7450+ " static INLINE void first(int *p) { p[0] = 0; }\n"
7451+ " static INLINE void second(int *p) { first(p); }\n"
7452+ "};\n"
7453+ "}\n"
7454+ "#endif\n",
7455+ CBM_LANG_CPP, "t", "box.h");
7456+ ASSERT_NOT_NULL(r);
7457+ int first_count = 0;
7458+ int second_count = 0;
7459+ const char *first_qn = NULL;
7460+ for (int i = 0; i < r->defs.count; i++) {
7461+ const CBMDefinition *d = &r->defs.items[i];
7462+ if (qn_has_token_spaced_box(d->qualified_name)) {
7463+ fprintf(stderr, " [tmplqn] FAIL token-spaced def QN: %s\n", d->qualified_name);
7464+ }
7465+ ASSERT_FALSE(qn_has_token_spaced_box(d->qualified_name));
7466+ if (d->name && strcmp(d->name, "first") == 0) {
7467+ first_count++;
7468+ first_qn = d->qualified_name;
7469+ }
7470+ if (d->name && strcmp(d->name, "second") == 0) {
7471+ second_count++;
7472+ }
7473+ }
7474+ ASSERT_EQ(first_count, 1);
7475+ ASSERT_EQ(second_count, 1);
7476+ ASSERT_NOT_NULL(first_qn);
7477+ ASSERT_NOT_NULL(strstr(first_qn, "ns.box<int, 2>.first"));
7478+ /* Call scopes and LSP-resolved endpoints name the same single spelling, so
7479+ * no CALLS edge lands on (or starts from) a phantom second node. */
7480+ for (int i = 0; i < r->calls.count; i++) {
7481+ ASSERT_FALSE(qn_has_token_spaced_box(r->calls.items[i].enclosing_func_qn));
7482+ }
7483+ for (int i = 0; i < r->resolved_calls.count; i++) {
7484+ const CBMResolvedCall *rc = &r->resolved_calls.items[i];
7485+ ASSERT_FALSE(qn_has_token_spaced_box(rc->caller_qn));
7486+ ASSERT_FALSE(qn_has_token_spaced_box(rc->callee_qn));
7487+ }
7488+ cbm_free_result(r);
7489+ PASS();
7490+ }
7491+
74277492/* #1989: build-system export macros (UE "<MOD>_API", CMake generate_export_header
74287493 * "<lib>_EXPORT") are empty on the real compile line but opaque to tree-sitter,
74297494 * so `class MOD_API Foo` misparses. The preprocessed second pass predefines the
@@ -12359,6 +12424,7 @@ SUITE(extraction) {
1235912424 RUN_TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949);
1236012425 RUN_TEST(extract_c_ifdef_split_brace_after_include_remapped_issue949);
1236112426 RUN_TEST(extract_c_clean_file_no_recovery_duplicates_issue961);
12427+ RUN_TEST(extract_cpp_template_specialization_preprocessed_qn_single_spelling);
1236212428 RUN_TEST(extract_cpp_export_macro_class_recovery_issue1989);
1236312429 RUN_TEST(extract_cpp_export_macro_struct_recovery_issue1989);
1236412430 RUN_TEST(extract_cpp_export_macro_enum_recovery_issue1989);
0 commit comments