diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index b655d06c1d..835bd88791 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -2055,12 +2055,6 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou return result; } -/* Rust: two same-named functions guarded by mutually-exclusive #[cfg(...)] - * attributes both parse as distinct function_item nodes and otherwise receive - * the SAME qualified_name, so the second graph upsert silently overwrites the - * first and one branch is lost (#495). Fold the cfg predicate into the QN so - * each cfg-gated twin gets a DISTINCT, predicate-encoding QN. Returns the - * (possibly suffixed) QN; the original QN when no cfg attribute is present. */ /* Rust: mark a function as a test when it carries a test attribute (#855). * cbm's test detection is otherwise file-path-based (cbm_is_test_file: * *_test.rs / test_*), so inline #[test]/#[tokio::test] functions inside a @@ -2090,25 +2084,58 @@ static bool rust_def_is_test(const char *const *decorators) { return false; } +/* Rust: two same-named functions guarded by mutually-exclusive #[cfg(...)] + * attributes both parse as distinct function_item nodes and otherwise receive + * the SAME qualified_name, so the second graph upsert silently overwrites the + * first and one branch is lost (#495). Fold the cfg predicate into the QN so + * each cfg-gated twin gets a DISTINCT, predicate-encoding QN. Returns the + * (possibly suffixed) QN; the original QN when no cfg attribute is present. */ +enum { ATTR_OPEN_CHARS = 2 }; /* the "#[" a bracketed attribute opens with */ + static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn, const char *const *decorators) { if (!decorators) { return base_qn; } for (int i = 0; decorators[i]; i++) { - const char *cfg = strstr(decorators[i], "cfg("); + /* The attribute must BE `cfg`, not merely contain that text. `cfg_attr` + * carries its own nested `cfg(...)`, and in the common + * `#[cfg_attr(docsrs, doc(cfg(feature = "x")))]` that inner predicate + * gates DOCUMENTATION, not compilation: the function is compiled + * unconditionally and has no twin to be told apart from. Anchor on the + * bracketed path, which is the convention rust_def_is_test above already + * follows for the same reason. */ + const char *cfg = strstr(decorators[i], "#[cfg("); if (!cfg) { continue; } - /* Build a compact predicate suffix from the cfg(...) text, dropping - * whitespace and quotes so the QN stays readable and stable. */ + cfg += ATTR_OPEN_CHARS; + /* Copy the BALANCED cfg(...) span, dropping whitespace and quotes so the + * QN stays readable and stable. Running to the end of the decorator text + * instead cost two things. It appended the attribute's own ']' to every + * QN. And on a multi-line attribute the copied text carried the newline, + * which cbm_defs_push cuts the QN at by design, leaving the suffix + * truncated mid-predicate: two twins whose predicates differ only past + * that newline then computed ONE qualified name and the second upsert + * overwrote the first, which is the loss this function exists to + * prevent (#495). + * + * A predicate longer than the buffer still truncates, as before. It + * stays distinct for the first 255 characters, which is what keeps the + * twins apart. */ char buf[CBM_SZ_256]; size_t bi = 0; + int depth = 0; for (const char *p = cfg; *p && bi + 1 < sizeof(buf); p++) { - if (*p == ' ' || *p == '\t' || *p == '"' || *p == '\'') { + if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == '"' || *p == '\'') { continue; } buf[bi++] = *p; + if (*p == '(') { + depth++; + } else if (*p == ')' && --depth == 0) { + break; + } } buf[bi] = '\0'; return cbm_arena_sprintf(a, "%s#%s", base_qn, buf); diff --git a/src/store/store.h b/src/store/store.h index 06ea1fbb7e..4611bfa411 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -23,7 +23,7 @@ typedef struct cbm_store cbm_store_t; #define CBM_STORE_OK 0 #define CBM_STORE_ERR (-1) #define CBM_STORE_NOT_FOUND (-2) -#define CBM_INDEX_FORMAT_VERSION 1 +#define CBM_INDEX_FORMAT_VERSION 2 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5a21fe3e2e..521dacb6ff 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -985,6 +985,63 @@ TEST(rust_function) { PASS(); } +/* The cfg fence exists so two mutually-exclusive #[cfg(...)] twins get distinct + * qualified names instead of one overwriting the other (#495). Three ways it + * misread the attribute, all visible in the QN it mints. */ +TEST(rust_cfg_fence_is_the_balanced_predicate_only) { + CBMFileResult *r = extract("#[cfg(test)]\n" + "pub fn gated() {}\n" + "\n" + "#[cfg(target_os = \"linux\")]\n" + "pub fn gated_os() {}\n", + CBM_LANG_RUST, "t", "src/lib.rs"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* The attribute's own closing bracket is not part of the predicate. */ + ASSERT_TRUE(has_def_qn(r, "t.src.lib.gated#cfg(test)")); + ASSERT_TRUE(has_def_qn(r, "t.src.lib.gated_os#cfg(target_os=linux)")); + cbm_free_result(r); + PASS(); +} + +/* `#[cfg_attr(docsrs, doc(cfg(...)))]` gates documentation, not compilation, so + * the function is compiled unconditionally and has no twin. An unanchored search + * for "cfg(" found the nested predicate and fenced it anyway. */ +TEST(rust_cfg_attr_doc_gate_mints_no_fence) { + CBMFileResult *r = extract("#[cfg_attr(docsrs, doc(cfg(feature = \"rt\")))]\n" + "pub async fn yield_now() {}\n", + CBM_LANG_RUST, "t", "src/lib.rs"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_TRUE(has_def_qn(r, "t.src.lib.yield_now")); + cbm_free_result(r); + PASS(); +} + +/* A multi-line attribute carried its newline into the QN, and cbm_defs_push cuts + * a qualified name at a line break by design, so both twins were left with the + * same truncated `#cfg(all(` and the second upsert overwrote the first. */ +TEST(rust_multiline_cfg_twins_stay_distinct) { + CBMFileResult *r = extract("#[cfg(all(\n" + " unix,\n" + " feature = \"alpha\"\n" + "))]\n" + "pub fn twin() -> u32 { 1 }\n" + "\n" + "#[cfg(all(\n" + " unix,\n" + " feature = \"beta\"\n" + "))]\n" + "pub fn twin() -> u32 { 2 }\n", + CBM_LANG_RUST, "t", "src/lib.rs"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_TRUE(has_def_qn(r, "t.src.lib.twin#cfg(all(unix,feature=alpha))")); + ASSERT_TRUE(has_def_qn(r, "t.src.lib.twin#cfg(all(unix,feature=beta))")); + cbm_free_result(r); + PASS(); +} + TEST(rust_struct) { CBMFileResult *r = extract("pub struct Point { pub x: f64, pub y: f64 }\nimpl Point { pub fn " "new(x: f64, y: f64) -> Self { Point { x, y } } }\n", @@ -8925,6 +8982,9 @@ SUITE(extraction) { /* Systems */ RUN_TEST(rust_function); + RUN_TEST(rust_cfg_fence_is_the_balanced_predicate_only); + RUN_TEST(rust_cfg_attr_doc_gate_mints_no_fence); + RUN_TEST(rust_multiline_cfg_twins_stay_distinct); RUN_TEST(rust_struct); RUN_TEST(go_function); RUN_TEST(go_struct);