Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -2041,12 +2041,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
Expand Down Expand Up @@ -2076,25 +2070,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);
Expand Down
2 changes: 1 addition & 1 deletion src/store/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,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",
Expand Down Expand Up @@ -8701,6 +8758,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);
Expand Down
Loading