From 7f1e80ce90d9fabe90c2cd3680d822d831a53d38 Mon Sep 17 00:00:00 2001 From: Harshita Joshi Date: Tue, 28 Jul 2026 20:11:09 -0500 Subject: [PATCH 1/4] fix(extract): prevent duplicate Function nodes for Java interface methods (#1234) Signed-off-by: Harshita Joshi --- internal/cbm/extract_defs.c | 1 + tests/test_extraction.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index dbc1d46b1..fc8f20f2d 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -6349,6 +6349,7 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || + strcmp(ck, "interface_body") == 0 || strcmp(ck, "enum_body") == 0 || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other diff --git a/tests/test_extraction.c b/tests/test_extraction.c index d6458e7d3..1f88cfc58 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -419,6 +419,35 @@ TEST(java_interface) { PASS(); } +/* Regression for #1234: Java interface methods were emitted as both a Method + * node (correct, via extract_class_methods) and a duplicate Function node + * (incorrect, via the walk_defs fallback). push_class_body_children did not + * recognize interface_body as a class body container, so method_declaration + * children were re-walked and extracted as top-level functions. */ +TEST(java_interface_no_duplicate_function_issue1234) { + CBMFileResult *r = + extract("public interface MarketplaceService {\n" + " ReservationDTO createReservation(Authentication auth, RequestDTO req);\n" + " void cancelReservation(long id);\n" + "}\n", + CBM_LANG_JAVA, "t", "MarketplaceService.java"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + /* The interface itself must exist. */ + ASSERT(has_def(r, "Interface", "MarketplaceService")); + + /* Each method must be a Method, not a Function. */ + ASSERT(has_def(r, "Method", "createReservation")); + ASSERT(has_def(r, "Method", "cancelReservation")); + + /* No Function nodes should exist for interface methods. */ + ASSERT_EQ(count_defs_with_label(r, "Function"), 0); + + cbm_free_result(r); + PASS(); +} + /* Regression for #279: a Java class declaring both `extends` and * `implements` must produce one INHERITS edge per base — the extends parent * AND every implements interface — with bare type names (not the keyword @@ -4947,6 +4976,7 @@ SUITE(extraction) { RUN_TEST(java_class); RUN_TEST(java_method); RUN_TEST(java_interface); + RUN_TEST(java_interface_no_duplicate_function_issue1234); RUN_TEST(java_class_extends_and_implements); RUN_TEST(python_class_base_extracted_bare); RUN_TEST(php_class); From 11ee490546bd083e7eb8986f80a7b1d3e0d076ad Mon Sep 17 00:00:00 2001 From: Harshita Joshi Date: Fri, 31 Jul 2026 01:00:13 -0500 Subject: [PATCH 2/4] fix(extract): merge-dedup Function nodes into Method nodes, preserving CALLS edges (#1234) Signed-off-by: Harshita Joshi --- internal/cbm/cbm.c | 2 + internal/cbm/cbm.h | 6 +++ internal/cbm/extract_defs.c | 70 +++++++++++++++++++++++++++++++++- tests/test_convergence_probe.c | 28 ++++++++++++++ tests/test_extraction.c | 41 +++++++++++++++----- 5 files changed, 137 insertions(+), 10 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index ee1749cdb..b6f21cc17 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -1239,6 +1239,8 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua cbm_extract_imports(&ctx); cbm_extract_unified(&ctx); + cbm_dedup_class_method_functions(result); + // Channel detection (Socket.IO / EventEmitter) — JS/TS only. cbm_extract_channels(&ctx); diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 28de37fb7..ff021ae6b 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -596,6 +596,12 @@ CBMFileResult *cbm_extract_file_ex( const CBMReturnTypeTable *return_type_table // OS return types, or NULL ); +// Merge-dedup: remove Function defs that duplicate a Method def at the same +// location, rewriting any call records that referenced the Function QN to use +// the surviving Method QN. Must run after both cbm_extract_definitions and +// cbm_extract_unified so that result->calls is populated. +void cbm_dedup_class_method_functions(CBMFileResult *result); + // Free all memory associated with a result. void cbm_free_result(CBMFileResult *result); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index fc8f20f2d..e4e5873e1 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -4440,6 +4440,25 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c } } + // Java enums wrap methods inside enum_body_declarations; iterate + // through to reach the method_declaration nodes. + if (strcmp(ts_node_type(child), "enum_body_declarations") == 0) { + uint32_t dc = ts_node_child_count(child); + for (uint32_t j = 0; j < dc; j++) { + TSNode dchild = ts_node_child(child, j); + if (ts_node_is_null(dchild) || + !cbm_kind_in_set(dchild, spec->function_node_types)) { + continue; + } + TSNode nn = resolve_method_name(dchild, ctx->language); + if (ts_node_is_null(nn)) { + continue; + } + push_method_def(ctx, dchild, class_node, class_qn, spec, nn); + } + continue; + } + // Python wraps @classmethod / @staticmethod / @property methods in // a decorated_definition node. Peek through it to find the inner // function_definition so we still emit a Method entry. @@ -6349,7 +6368,6 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || - strcmp(ck, "interface_body") == 0 || strcmp(ck, "enum_body") == 0 || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other @@ -6915,3 +6933,53 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) { // Extract module-level variables extract_variables(ctx, ctx->root, spec); } + +void cbm_dedup_class_method_functions(CBMFileResult *result) { + if (!result || result->defs.count < 2) { + return; + } + CBMDefArray *defs = &result->defs; + for (int fi = defs->count - 1; fi >= 0; fi--) { + CBMDefinition *func = &defs->items[fi]; + if (!func->label || strcmp(func->label, "Function") != 0) { + continue; + } + if (!func->name || !func->file_path) { + continue; + } + for (int mi = 0; mi < defs->count; mi++) { + if (mi == fi) { + continue; + } + CBMDefinition *method = &defs->items[mi]; + if (!method->label || strcmp(method->label, "Method") != 0) { + continue; + } + if (!method->name || strcmp(method->name, func->name) != 0) { + continue; + } + if (!method->file_path || strcmp(method->file_path, func->file_path) != 0) { + continue; + } + if (method->start_line != func->start_line) { + continue; + } + const char *func_qn = func->qualified_name; + const char *method_qn = method->qualified_name; + if (func_qn && method_qn) { + for (int ci = 0; ci < result->calls.count; ci++) { + CBMCall *call = &result->calls.items[ci]; + if (call->enclosing_func_qn && strcmp(call->enclosing_func_qn, func_qn) == 0) { + call->enclosing_func_qn = method_qn; + } + } + } + if (fi < defs->count - 1) { + memmove(&defs->items[fi], &defs->items[fi + 1], + (size_t)(defs->count - fi - 1) * sizeof(defs->items[0])); + } + defs->count--; + break; + } + } +} diff --git a/tests/test_convergence_probe.c b/tests/test_convergence_probe.c index b530f60ce..4e9454e6c 100644 --- a/tests/test_convergence_probe.c +++ b/tests/test_convergence_probe.c @@ -1144,6 +1144,32 @@ TEST(cp_enum_method_java) { PASS(); } +/* D6d — Java interface method call after dedup (#1234). + * cbm_dedup_class_method_functions merges duplicate Function nodes into + * Method nodes for interface/enum bodies. The surviving Method must still + * carry CALLS edges — dedup must be a merge, not a delete. */ +TEST(cp_interface_method_calls_after_dedup) { + static const CP_File f[] = { + {"Svc.java", + "package app;\n\n" + "interface Greeter {\n" + " String greet(String name);\n}\n\n" + "class SimpleGreeter implements Greeter {\n" + " public String greet(String name) { return \"Hello \" + name; }\n}\n\n" + "class App {\n" + " static String run(Greeter g) {\n" + " return g.greet(\"world\");\n }\n}\n"}}; + CP_Proj lp; + cbm_store_t *store = cp_index_files(&lp, f, 1); + int calls = cp_edges(store, lp.project, "CALLS"); + int fns = cp_count_label(store, lp.project, "Function"); + if (calls < 1) cp_diag(store, lp.project, "interface_dedup/java_1234"); + cp_cleanup(&lp, store); + ASSERT_TRUE(fns == 0); + ASSERT_TRUE(calls >= 1); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════ * AREA E — Node-creation corners * ══════════════════════════════════════════════════════════════════ */ @@ -1574,6 +1600,8 @@ SUITE(convergence_probe) { RUN_TEST(cp_enum_variant_rust_impl); /* D6c Java enum method — EXPECTED UNCERTAIN */ RUN_TEST(cp_enum_method_java); + /* D6d Java interface dedup — #1234 regression */ + RUN_TEST(cp_interface_method_calls_after_dedup); /* ── AREA E1: Nested classes (3 cases) ──────────── */ /* E1a Python inner class — EXPECTED GREEN */ diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 1f88cfc58..edeebf0c3 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -419,11 +419,12 @@ TEST(java_interface) { PASS(); } -/* Regression for #1234: Java interface methods were emitted as both a Method - * node (correct, via extract_class_methods) and a duplicate Function node - * (incorrect, via the walk_defs fallback). push_class_body_children did not - * recognize interface_body as a class body container, so method_declaration - * children were re-walked and extracted as top-level functions. */ +/* Regression for #1234: Java interface/enum methods were emitted as both a + * Method node (correct, via extract_class_methods) and a duplicate Function + * node (incorrect, via walk_defs). cbm_dedup_class_method_functions merges + * the duplicate by removing the Function and rewriting any call records whose + * enclosing_func_qn referenced the Function QN to use the surviving Method QN. + * This test covers both interface and enum bodies. */ TEST(java_interface_no_duplicate_function_issue1234) { CBMFileResult *r = extract("public interface MarketplaceService {\n" @@ -434,16 +435,37 @@ TEST(java_interface_no_duplicate_function_issue1234) { ASSERT_NOT_NULL(r); ASSERT_FALSE(r->has_error); - /* The interface itself must exist. */ ASSERT(has_def(r, "Interface", "MarketplaceService")); - - /* Each method must be a Method, not a Function. */ ASSERT(has_def(r, "Method", "createReservation")); ASSERT(has_def(r, "Method", "cancelReservation")); + ASSERT_EQ(count_defs_with_label(r, "Function"), 0); + + cbm_free_result(r); + PASS(); +} - /* No Function nodes should exist for interface methods. */ +TEST(java_enum_dedup_preserves_calls_issue1234) { + CBMFileResult *r = + extract("package app;\n\nenum Day {\n" + " MON, TUE, WED, THU, FRI, SAT, SUN;\n\n" + " public boolean isWeekend() { return this == SAT || this == SUN; }\n" + " public String label() { return name().toLowerCase(); }\n}\n\n" + "class DayUtil {\n" + " static String describe(Day d) {\n" + " return d.label() + (d.isWeekend() ? \"(rest)\" : \"(work)\");\n }\n}\n", + CBM_LANG_JAVA, "t", "Day.java"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + ASSERT(has_def(r, "Enum", "Day")); + ASSERT(has_def(r, "Method", "isWeekend")); + ASSERT(has_def(r, "Method", "label")); + ASSERT(has_def(r, "Class", "DayUtil")); + ASSERT(has_def(r, "Method", "describe")); ASSERT_EQ(count_defs_with_label(r, "Function"), 0); + ASSERT_TRUE(r->calls.count >= 1); + cbm_free_result(r); PASS(); } @@ -4977,6 +4999,7 @@ SUITE(extraction) { RUN_TEST(java_method); RUN_TEST(java_interface); RUN_TEST(java_interface_no_duplicate_function_issue1234); + RUN_TEST(java_enum_dedup_preserves_calls_issue1234); RUN_TEST(java_class_extends_and_implements); RUN_TEST(python_class_base_extracted_bare); RUN_TEST(php_class); From 7945c3ad52a3b750c063ca6e60d6b30b07289c19 Mon Sep 17 00:00:00 2001 From: Harshita Joshi Date: Fri, 31 Jul 2026 01:36:44 -0500 Subject: [PATCH 3/4] test: drop non-binding calls.count assertion from enum dedup test Signed-off-by: Harshita Joshi --- tests/test_extraction.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index edeebf0c3..864d77071 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -464,8 +464,6 @@ TEST(java_enum_dedup_preserves_calls_issue1234) { ASSERT(has_def(r, "Method", "describe")); ASSERT_EQ(count_defs_with_label(r, "Function"), 0); - ASSERT_TRUE(r->calls.count >= 1); - cbm_free_result(r); PASS(); } From 35a9b33a358206a5230f68a2e35a9880d0fdee6e Mon Sep 17 00:00:00 2001 From: Harshita Joshi Date: Fri, 31 Jul 2026 12:44:06 -0500 Subject: [PATCH 4/4] fix(extract): replace dedup pass with Java-gated prevention in push_class_body_children Signed-off-by: Harshita Joshi --- internal/cbm/cbm.c | 2 -- internal/cbm/cbm.h | 6 ---- internal/cbm/extract_defs.c | 54 +++------------------------------- tests/test_convergence_probe.c | 14 ++++----- tests/test_extraction.c | 8 ++--- 5 files changed, 15 insertions(+), 69 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index b6f21cc17..ee1749cdb 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -1239,8 +1239,6 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua cbm_extract_imports(&ctx); cbm_extract_unified(&ctx); - cbm_dedup_class_method_functions(result); - // Channel detection (Socket.IO / EventEmitter) — JS/TS only. cbm_extract_channels(&ctx); diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index ff021ae6b..28de37fb7 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -596,12 +596,6 @@ CBMFileResult *cbm_extract_file_ex( const CBMReturnTypeTable *return_type_table // OS return types, or NULL ); -// Merge-dedup: remove Function defs that duplicate a Method def at the same -// location, rewriting any call records that referenced the Function QN to use -// the surviving Method QN. Must run after both cbm_extract_definitions and -// cbm_extract_unified so that result->calls is populated. -void cbm_dedup_class_method_functions(CBMFileResult *result); - // Free all memory associated with a result. void cbm_free_result(CBMFileResult *result); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index e4e5873e1..2cb19fadc 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -6368,6 +6368,10 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || + // Java interface/enum bodies: gated to Java so grammars that + // share these node names (TypeScript, Apex, Dart) are unaffected. + (strcmp(ck, "interface_body") == 0 && spec->language == CBM_LANG_JAVA) || + (strcmp(ck, "enum_body") == 0 && spec->language == CBM_LANG_JAVA) || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other @@ -6933,53 +6937,3 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) { // Extract module-level variables extract_variables(ctx, ctx->root, spec); } - -void cbm_dedup_class_method_functions(CBMFileResult *result) { - if (!result || result->defs.count < 2) { - return; - } - CBMDefArray *defs = &result->defs; - for (int fi = defs->count - 1; fi >= 0; fi--) { - CBMDefinition *func = &defs->items[fi]; - if (!func->label || strcmp(func->label, "Function") != 0) { - continue; - } - if (!func->name || !func->file_path) { - continue; - } - for (int mi = 0; mi < defs->count; mi++) { - if (mi == fi) { - continue; - } - CBMDefinition *method = &defs->items[mi]; - if (!method->label || strcmp(method->label, "Method") != 0) { - continue; - } - if (!method->name || strcmp(method->name, func->name) != 0) { - continue; - } - if (!method->file_path || strcmp(method->file_path, func->file_path) != 0) { - continue; - } - if (method->start_line != func->start_line) { - continue; - } - const char *func_qn = func->qualified_name; - const char *method_qn = method->qualified_name; - if (func_qn && method_qn) { - for (int ci = 0; ci < result->calls.count; ci++) { - CBMCall *call = &result->calls.items[ci]; - if (call->enclosing_func_qn && strcmp(call->enclosing_func_qn, func_qn) == 0) { - call->enclosing_func_qn = method_qn; - } - } - } - if (fi < defs->count - 1) { - memmove(&defs->items[fi], &defs->items[fi + 1], - (size_t)(defs->count - fi - 1) * sizeof(defs->items[0])); - } - defs->count--; - break; - } - } -} diff --git a/tests/test_convergence_probe.c b/tests/test_convergence_probe.c index 4e9454e6c..66cc77135 100644 --- a/tests/test_convergence_probe.c +++ b/tests/test_convergence_probe.c @@ -1144,11 +1144,11 @@ TEST(cp_enum_method_java) { PASS(); } -/* D6d — Java interface method call after dedup (#1234). - * cbm_dedup_class_method_functions merges duplicate Function nodes into - * Method nodes for interface/enum bodies. The surviving Method must still - * carry CALLS edges — dedup must be a merge, not a delete. */ -TEST(cp_interface_method_calls_after_dedup) { +/* D6d — Java interface method call (#1234). + * Prevention in push_class_body_children stops duplicate Function nodes + * from being minted for interface methods. The surviving Method node + * must still carry its CALLS edge. */ +TEST(cp_interface_method_no_dup_function) { static const CP_File f[] = { {"Svc.java", "package app;\n\n" @@ -1600,8 +1600,8 @@ SUITE(convergence_probe) { RUN_TEST(cp_enum_variant_rust_impl); /* D6c Java enum method — EXPECTED UNCERTAIN */ RUN_TEST(cp_enum_method_java); - /* D6d Java interface dedup — #1234 regression */ - RUN_TEST(cp_interface_method_calls_after_dedup); + /* D6d Java interface method — #1234 regression */ + RUN_TEST(cp_interface_method_no_dup_function); /* ── AREA E1: Nested classes (3 cases) ──────────── */ /* E1a Python inner class — EXPECTED GREEN */ diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 864d77071..648607d1a 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -421,10 +421,10 @@ TEST(java_interface) { /* Regression for #1234: Java interface/enum methods were emitted as both a * Method node (correct, via extract_class_methods) and a duplicate Function - * node (incorrect, via walk_defs). cbm_dedup_class_method_functions merges - * the duplicate by removing the Function and rewriting any call records whose - * enclosing_func_qn referenced the Function QN to use the surviving Method QN. - * This test covers both interface and enum bodies. */ + * node (incorrect, via walk_defs). Prevention in push_class_body_children + * (gated to Java) recognizes interface_body and enum_body as class body + * containers, stopping the fallback path from re-walking method_declaration + * children as top-level functions. */ TEST(java_interface_no_duplicate_function_issue1234) { CBMFileResult *r = extract("public interface MarketplaceService {\n"