diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index dbc1d46b1..2cb19fadc 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,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 diff --git a/tests/test_convergence_probe.c b/tests/test_convergence_probe.c index b530f60ce..66cc77135 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 (#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" + "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 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 d6458e7d3..648607d1a 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -419,6 +419,55 @@ TEST(java_interface) { PASS(); } +/* 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). 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" + " 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); + + ASSERT(has_def(r, "Interface", "MarketplaceService")); + 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(); +} + +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); + + 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 +4996,8 @@ 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_enum_dedup_preserves_calls_issue1234); RUN_TEST(java_class_extends_and_implements); RUN_TEST(python_class_base_extracted_bare); RUN_TEST(php_class);