Skip to content

Commit 0cde6ba

Browse files
fix(extract): merge-dedup Function nodes into Method nodes, preserving CALLS edges (#1234)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
1 parent 7f1e80c commit 0cde6ba

5 files changed

Lines changed: 138 additions & 10 deletions

File tree

‎internal/cbm/cbm.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,8 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
12391239
cbm_extract_imports(&ctx);
12401240
cbm_extract_unified(&ctx);
12411241

1242+
cbm_dedup_class_method_functions(result);
1243+
12421244
// Channel detection (Socket.IO / EventEmitter) — JS/TS only.
12431245
cbm_extract_channels(&ctx);
12441246

‎internal/cbm/cbm.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,12 @@ CBMFileResult *cbm_extract_file_ex(
596596
const CBMReturnTypeTable *return_type_table // OS return types, or NULL
597597
);
598598

599+
// Merge-dedup: remove Function defs that duplicate a Method def at the same
600+
// location, rewriting any call records that referenced the Function QN to use
601+
// the surviving Method QN. Must run after both cbm_extract_definitions and
602+
// cbm_extract_unified so that result->calls is populated.
603+
void cbm_dedup_class_method_functions(CBMFileResult *result);
604+
599605
// Free all memory associated with a result.
600606
void cbm_free_result(CBMFileResult *result);
601607

‎internal/cbm/extract_defs.c‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4440,6 +4440,25 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
44404440
}
44414441
}
44424442

4443+
// Java enums wrap methods inside enum_body_declarations; iterate
4444+
// through to reach the method_declaration nodes.
4445+
if (strcmp(ts_node_type(child), "enum_body_declarations") == 0) {
4446+
uint32_t dc = ts_node_child_count(child);
4447+
for (uint32_t j = 0; j < dc; j++) {
4448+
TSNode dchild = ts_node_child(child, j);
4449+
if (ts_node_is_null(dchild) ||
4450+
!cbm_kind_in_set(dchild, spec->function_node_types)) {
4451+
continue;
4452+
}
4453+
TSNode nn = resolve_method_name(dchild, ctx->language);
4454+
if (ts_node_is_null(nn)) {
4455+
continue;
4456+
}
4457+
push_method_def(ctx, dchild, class_node, class_qn, spec, nn);
4458+
}
4459+
continue;
4460+
}
4461+
44434462
// Python wraps @classmethod / @staticmethod / @property methods in
44444463
// a decorated_definition node. Peek through it to find the inner
44454464
// 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
63496368
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
63506369
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
63516370
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
6352-
strcmp(ck, "interface_body") == 0 || strcmp(ck, "enum_body") == 0 ||
63536371
// Groovy class bodies are a `closure` node; routing through the
63546372
// nested-class path keeps methods from being re-walked (and thus
63556373
// double-extracted) as top-level functions. Gated to Groovy so other
@@ -6915,3 +6933,54 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
69156933
// Extract module-level variables
69166934
extract_variables(ctx, ctx->root, spec);
69176935
}
6936+
6937+
void cbm_dedup_class_method_functions(CBMFileResult *result) {
6938+
if (!result || result->defs.count < 2) {
6939+
return;
6940+
}
6941+
CBMDefArray *defs = &result->defs;
6942+
for (int fi = defs->count - 1; fi >= 0; fi--) {
6943+
CBMDefinition *func = &defs->items[fi];
6944+
if (!func->label || strcmp(func->label, "Function") != 0) {
6945+
continue;
6946+
}
6947+
if (!func->name || !func->file_path) {
6948+
continue;
6949+
}
6950+
for (int mi = 0; mi < defs->count; mi++) {
6951+
if (mi == fi) {
6952+
continue;
6953+
}
6954+
CBMDefinition *method = &defs->items[mi];
6955+
if (!method->label || strcmp(method->label, "Method") != 0) {
6956+
continue;
6957+
}
6958+
if (!method->name || strcmp(method->name, func->name) != 0) {
6959+
continue;
6960+
}
6961+
if (!method->file_path || strcmp(method->file_path, func->file_path) != 0) {
6962+
continue;
6963+
}
6964+
if (method->start_line != func->start_line) {
6965+
continue;
6966+
}
6967+
const char *func_qn = func->qualified_name;
6968+
const char *method_qn = method->qualified_name;
6969+
if (func_qn && method_qn) {
6970+
for (int ci = 0; ci < result->calls.count; ci++) {
6971+
CBMCall *call = &result->calls.items[ci];
6972+
if (call->enclosing_func_qn &&
6973+
strcmp(call->enclosing_func_qn, func_qn) == 0) {
6974+
call->enclosing_func_qn = method_qn;
6975+
}
6976+
}
6977+
}
6978+
if (fi < defs->count - 1) {
6979+
memmove(&defs->items[fi], &defs->items[fi + 1],
6980+
(size_t)(defs->count - fi - 1) * sizeof(defs->items[0]));
6981+
}
6982+
defs->count--;
6983+
break;
6984+
}
6985+
}
6986+
}

‎tests/test_convergence_probe.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,32 @@ TEST(cp_enum_method_java) {
11441144
PASS();
11451145
}
11461146

1147+
/* D6d — Java interface method call after dedup (#1234).
1148+
* cbm_dedup_class_method_functions merges duplicate Function nodes into
1149+
* Method nodes for interface/enum bodies. The surviving Method must still
1150+
* carry CALLS edges — dedup must be a merge, not a delete. */
1151+
TEST(cp_interface_method_calls_after_dedup) {
1152+
static const CP_File f[] = {
1153+
{"Svc.java",
1154+
"package app;\n\n"
1155+
"interface Greeter {\n"
1156+
" String greet(String name);\n}\n\n"
1157+
"class SimpleGreeter implements Greeter {\n"
1158+
" public String greet(String name) { return \"Hello \" + name; }\n}\n\n"
1159+
"class App {\n"
1160+
" static String run(Greeter g) {\n"
1161+
" return g.greet(\"world\");\n }\n}\n"}};
1162+
CP_Proj lp;
1163+
cbm_store_t *store = cp_index_files(&lp, f, 1);
1164+
int calls = cp_edges(store, lp.project, "CALLS");
1165+
int fns = cp_count_label(store, lp.project, "Function");
1166+
if (calls < 1) cp_diag(store, lp.project, "interface_dedup/java_1234");
1167+
cp_cleanup(&lp, store);
1168+
ASSERT_TRUE(fns == 0);
1169+
ASSERT_TRUE(calls >= 1);
1170+
PASS();
1171+
}
1172+
11471173
/* ══════════════════════════════════════════════════════════════════
11481174
* AREA E — Node-creation corners
11491175
* ══════════════════════════════════════════════════════════════════ */
@@ -1574,6 +1600,8 @@ SUITE(convergence_probe) {
15741600
RUN_TEST(cp_enum_variant_rust_impl);
15751601
/* D6c Java enum method — EXPECTED UNCERTAIN */
15761602
RUN_TEST(cp_enum_method_java);
1603+
/* D6d Java interface dedup — #1234 regression */
1604+
RUN_TEST(cp_interface_method_calls_after_dedup);
15771605

15781606
/* ── AREA E1: Nested classes (3 cases) ──────────── */
15791607
/* E1a Python inner class — EXPECTED GREEN */

‎tests/test_extraction.c‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,12 @@ TEST(java_interface) {
419419
PASS();
420420
}
421421

422-
/* Regression for #1234: Java interface methods were emitted as both a Method
423-
* node (correct, via extract_class_methods) and a duplicate Function node
424-
* (incorrect, via the walk_defs fallback). push_class_body_children did not
425-
* recognize interface_body as a class body container, so method_declaration
426-
* children were re-walked and extracted as top-level functions. */
422+
/* Regression for #1234: Java interface/enum methods were emitted as both a
423+
* Method node (correct, via extract_class_methods) and a duplicate Function
424+
* node (incorrect, via walk_defs). cbm_dedup_class_method_functions merges
425+
* the duplicate by removing the Function and rewriting any call records whose
426+
* enclosing_func_qn referenced the Function QN to use the surviving Method QN.
427+
* This test covers both interface and enum bodies. */
427428
TEST(java_interface_no_duplicate_function_issue1234) {
428429
CBMFileResult *r =
429430
extract("public interface MarketplaceService {\n"
@@ -434,16 +435,37 @@ TEST(java_interface_no_duplicate_function_issue1234) {
434435
ASSERT_NOT_NULL(r);
435436
ASSERT_FALSE(r->has_error);
436437

437-
/* The interface itself must exist. */
438438
ASSERT(has_def(r, "Interface", "MarketplaceService"));
439-
440-
/* Each method must be a Method, not a Function. */
441439
ASSERT(has_def(r, "Method", "createReservation"));
442440
ASSERT(has_def(r, "Method", "cancelReservation"));
441+
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
442+
443+
cbm_free_result(r);
444+
PASS();
445+
}
443446

444-
/* No Function nodes should exist for interface methods. */
447+
TEST(java_enum_dedup_preserves_calls_issue1234) {
448+
CBMFileResult *r =
449+
extract("package app;\n\nenum Day {\n"
450+
" MON, TUE, WED, THU, FRI, SAT, SUN;\n\n"
451+
" public boolean isWeekend() { return this == SAT || this == SUN; }\n"
452+
" public String label() { return name().toLowerCase(); }\n}\n\n"
453+
"class DayUtil {\n"
454+
" static String describe(Day d) {\n"
455+
" return d.label() + (d.isWeekend() ? \"(rest)\" : \"(work)\");\n }\n}\n",
456+
CBM_LANG_JAVA, "t", "Day.java");
457+
ASSERT_NOT_NULL(r);
458+
ASSERT_FALSE(r->has_error);
459+
460+
ASSERT(has_def(r, "Enum", "Day"));
461+
ASSERT(has_def(r, "Method", "isWeekend"));
462+
ASSERT(has_def(r, "Method", "label"));
463+
ASSERT(has_def(r, "Class", "DayUtil"));
464+
ASSERT(has_def(r, "Method", "describe"));
445465
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
446466

467+
ASSERT_TRUE(r->calls.count >= 1);
468+
447469
cbm_free_result(r);
448470
PASS();
449471
}
@@ -4977,6 +4999,7 @@ SUITE(extraction) {
49774999
RUN_TEST(java_method);
49785000
RUN_TEST(java_interface);
49795001
RUN_TEST(java_interface_no_duplicate_function_issue1234);
5002+
RUN_TEST(java_enum_dedup_preserves_calls_issue1234);
49805003
RUN_TEST(java_class_extends_and_implements);
49815004
RUN_TEST(python_class_base_extracted_bare);
49825005
RUN_TEST(php_class);

0 commit comments

Comments
 (0)