diff --git a/Makefile.cbm b/Makefile.cbm index 4c8b6eb65..c60e8fc69 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -408,7 +408,8 @@ TEST_REPRO_SRCS = \ tests/repro/repro_issue514.c \ tests/repro/repro_issue510.c \ tests/repro/repro_issue557.c \ - tests/repro/repro_issue520.c + tests/repro/repro_issue520.c \ + tests/repro/repro_issue333.c ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS) diff --git a/tests/repro/repro_issue333.c b/tests/repro/repro_issue333.c new file mode 100644 index 000000000..aedfc68d1 --- /dev/null +++ b/tests/repro/repro_issue333.c @@ -0,0 +1,251 @@ +/* + * repro_issue333.c — Reproduce-first case for OPEN bug #333. + * + * Bug #333: "Silent index degradation — status:'indexed' but only ~500 nodes + * for 72k LOC Rust" (reclassified as Rust extraction-depth gap). + * + * ROOT CAUSE — push_nested_class_nodes silently drops trait method defs: + * When the definition walker encounters a Rust `trait_item` node it is + * classified as a class (label "Interface") and `push_class_body_children` + * is called to schedule its children for further traversal. + * `push_class_body_children` finds the `declaration_list` body node (the + * Rust grammar's name for a trait body) and delegates to + * `push_nested_class_nodes` (extract_defs.c ~line 4890). + * `push_nested_class_nodes` only re-queues children that are in + * `spec->class_node_types` (struct_item, enum_item, etc.) or are named + * "field_declaration" / "template_declaration" / "declaration". + * It does NOT re-queue `function_item` or `function_signature_item` nodes. + * Therefore every method defined inside a trait body — both abstract + * declarations (function_signature_item, e.g. `fn area(&self) -> f64;`) + * and default implementations (function_item, e.g. `fn describe(&self) {}`) + * — is silently dropped and never reaches `extract_func_def`. + * + * EXPECTED (correct) behaviour: + * Extracting a Rust source file that defines a trait with methods must + * produce: + * - The trait itself as label "Interface" (already works). + * - Every method declared in the trait body as label "Method" (broken). + * Specifically for the fixture below: + * - Trait "Shape" → Interface node (already present) + * - Abstract method "area" inside trait Shape → Method node (MISSING) + * - Abstract method "perimeter" inside trait Shape → Method node (MISSING) + * - Default method "describe" inside trait Shape → Method node (MISSING) + * + * ACTUAL (buggy) behaviour: + * `r->defs` contains the Interface node for Shape but zero Method nodes + * for the three methods declared in its body. The ASSERT_EQ(3, ...) below + * evaluates to ASSERT_EQ(3, 0) and FAILs → RED. + * + * NOT covered by existing tests: + * - test_extraction.c::rust_struct tests `impl` block methods via the + * separate `extract_rust_impl` path, which is NOT affected by this bug. + * - test_rust_lsp.c trait tests (rustlsp_cov_trait_simple_method, etc.) + * only check `r->resolved_calls` (the LSP layer), never `r->defs`, so + * they do not detect missing trait-method def nodes. + * - test_matrix_new_constructs.c::mn_multiple_trait_bounds_rust tests a + * function with trait BOUNDS, not a trait DEFINITION with methods. + * No existing test asserts that method definitions inside a Rust `trait` + * body appear in `r->defs` — this is the first. + * + * FIX LOCATION: + * `push_nested_class_nodes` in internal/cbm/extract_defs.c (~line 4900): + * add `function_item` and `function_signature_item` to the set of node + * kinds that are re-queued onto the walk stack (or, equivalently, handle + * Rust `declaration_list` bodies via the same function-dispatch path used + * by `extract_rust_impl` for `impl_item` bodies). + */ + +#include "test_framework.h" +#include "cbm.h" + +/* + * count_method_defs_named — count defs with label "Method" matching name. + * Mirrors the `has_def` helper in test_extraction.c but counts all matches. + */ +static int count_method_defs_named(CBMFileResult *r, const char *name) { + int n = 0; + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *d = &r->defs.items[i]; + if (d->label && strcmp(d->label, "Method") == 0 && + d->name && strcmp(d->name, name) == 0) { + n++; + } + } + return n; +} + +/* + * count_defs_with_label — count all defs carrying the given label. + * Mirrors the helper in test_extraction.c. + */ +static int count_defs_with_label_local(CBMFileResult *r, const char *label) { + int n = 0; + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].label && strcmp(r->defs.items[i].label, label) == 0) + n++; + } + return n; +} + +/* ── Test ───────────────────────────────────────────────────────────────── */ + +/* + * repro_issue333_rust_extraction_depth + * + * Dense fixture: one trait "Shape" with two abstract methods (function_signature_item) + * and one default method (function_item), plus one concrete struct + impl block that + * implements the trait. The impl-block methods are extracted correctly via the + * existing `extract_rust_impl` path — this test asserts the TRAIT-BODY methods + * (not the impl methods) are also extracted. + * + * RED condition: + * count_defs_with_label(r, "Method") == 0 for methods INSIDE the trait body. + * Specifically, ASSERT_EQ(3, total_trait_methods) FAILs → 3 != 0. + * + * GREEN condition (after fix): + * "area", "perimeter", and "describe" each appear as a "Method" def node, + * all carrying parent_class pointing at the Shape trait. + */ +TEST(repro_issue333_rust_extraction_depth) { + /* + * Fixture: trait Shape with three methods. + * + * fn area — abstract (no body); grammar node: function_signature_item + * fn perimeter — abstract (no body); grammar node: function_signature_item + * fn describe — default implementation; grammar node: function_item + * + * Plus a struct Circle that implements Shape via an impl block. + * The impl-block methods (Circle::area, Circle::perimeter) are already + * extracted correctly; they serve as a positive control. + */ + static const char src[] = + "pub trait Shape {\n" + " fn area(&self) -> f64;\n" + " fn perimeter(&self) -> f64;\n" + " fn describe(&self) -> String {\n" + " format!(\"area={:.2} perimeter={:.2}\", self.area(), self.perimeter())\n" + " }\n" + "}\n" + "\n" + "pub struct Circle {\n" + " pub radius: f64,\n" + "}\n" + "\n" + "impl Shape for Circle {\n" + " fn area(&self) -> f64 {\n" + " std::f64::consts::PI * self.radius * self.radius\n" + " }\n" + " fn perimeter(&self) -> f64 {\n" + " 2.0 * std::f64::consts::PI * self.radius\n" + " }\n" + "}\n" + "\n" + "pub fn summarize(s: &dyn Shape) -> String {\n" + " s.describe()\n" + "}\n"; + + CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), + CBM_LANG_RUST, "t", "lib.rs", + 0, NULL, NULL); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + /* + * ASSERT 1 — Shape trait itself is extracted as Interface (positive control; + * already GREEN, confirms the trait node is at least parsed). + */ + int has_shape_interface = 0; + for (int i = 0; i < r->defs.count; i++) { + if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Interface") == 0 && + r->defs.items[i].name && strcmp(r->defs.items[i].name, "Shape") == 0) { + has_shape_interface = 1; + break; + } + } + ASSERT_TRUE(has_shape_interface); + + /* + * ASSERT 2 — Abstract trait methods appear as Method defs (the bug). + * + * `area` and `perimeter` are function_signature_item nodes (no body — + * just a declaration ending in `;`). `push_nested_class_nodes` never + * re-queues them because they are not class-type nodes, so they are + * dropped entirely. + * + * EXPECTED: 1 each. + * ACTUAL (buggy): 0 each — RED. + */ + int n_area = count_method_defs_named(r, "area"); + int n_perimeter = count_method_defs_named(r, "perimeter"); + + /* + * ASSERT 3 — Default trait method appears as Method def (also the bug). + * + * `describe` is a function_item node (has a body). Same gap: the walker + * never visits it because push_nested_class_nodes filters it out. + * + * EXPECTED: 1. + * ACTUAL (buggy): 0 — RED. + * + * NOTE: impl Circle also defines `area` and `perimeter` via extract_rust_impl, + * so those DO appear (as Methods with parent_class=Circle). We count the + * "describe" method separately to isolate the trait-body path — Circle never + * overrides `describe`, so any "describe" Method must come from the trait body. + */ + int n_describe = count_method_defs_named(r, "describe"); + + /* + * Total trait-body Methods that must appear: area + perimeter + describe = 3. + * + * Note: impl Circle provides its OWN area and perimeter Methods, so after the + * fix the total for "area" would be >= 2 (1 from trait + 1 from impl). We + * use >= 1 per name to be unambiguous about which path is broken. + * + * The single combined assertion for RED/GREEN clarity: + * int total_trait_methods = (n_area >= 1 ? 1 : 0) + * + (n_perimeter >= 1 ? 1 : 0) + * + (n_describe >= 1 ? 1 : 0); + * ASSERT_EQ(total_trait_methods, 3); + * + * On buggy code : total_trait_methods == 0 → ASSERT_EQ(0, 3) FAILS → RED + * After fix (area from trait body, perimeter from trait body, describe from + * trait body all present): total_trait_methods == 3 → ASSERT_EQ(3, 3) → GREEN + */ + int total_trait_methods = (n_area >= 1 ? 1 : 0) + + (n_perimeter >= 1 ? 1 : 0) + + (n_describe >= 1 ? 1 : 0); + + if (total_trait_methods < 3) { + printf(" DEBUG defs dump (total=%d):\n", r->defs.count); + for (int i = 0; i < r->defs.count; i++) { + printf(" [%d] label=%s name=%s\n", i, + r->defs.items[i].label ? r->defs.items[i].label : "(null)", + r->defs.items[i].name ? r->defs.items[i].name : "(null)"); + } + printf(" MISSING trait-body Method defs: " + "area=%d perimeter=%d describe=%d (need all 3)\n", + n_area, n_perimeter, n_describe); + } + + ASSERT_EQ(total_trait_methods, 3); + + /* + * Supplementary: count ALL Method defs present. + * After the fix we expect at least 5: + * trait body: area (abstract), perimeter (abstract), describe (default) + * impl Circle: area (concrete), perimeter (concrete) + * On buggy code: only the 2 impl-Circle methods are present → 2. + * We assert >= 3 here (conservative floor) rather than == 5 to stay + * focused on the trait-body gap and not break if the count changes. + */ + int total_methods = count_defs_with_label_local(r, "Method"); + ASSERT_GTE(total_methods, 3); + + cbm_free_result(r); + PASS(); +} + +/* ── Suite ──────────────────────────────────────────────────────────────── */ +SUITE(repro_issue333) { + RUN_TEST(repro_issue333_rust_extraction_depth); +} diff --git a/tests/repro/repro_main.c b/tests/repro/repro_main.c index 042f9aedd..d8904b5b4 100644 --- a/tests/repro/repro_main.c +++ b/tests/repro/repro_main.c @@ -43,6 +43,7 @@ extern void suite_repro_issue514(void); extern void suite_repro_issue510(void); extern void suite_repro_issue557(void); extern void suite_repro_issue520(void); +extern void suite_repro_issue333(void); int main(void) { /* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a @@ -73,6 +74,7 @@ int main(void) { RUN_SUITE(repro_issue510); RUN_SUITE(repro_issue557); RUN_SUITE(repro_issue520); + RUN_SUITE(repro_issue333); TEST_SUMMARY(); }