Skip to content
Merged
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
30 changes: 30 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,35 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
* 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
* regular .rs file are indexed as ordinary Functions (is_test=false) and leak
* past the store.c `is_test != 1` filter into graph/agent context. The
* attribute_item text extract_decorators stores is the bracketed form
* ("#[test]", "#[tokio::test]", "#[tokio::test(...)]", ...). */
static bool rust_def_is_test(const char *const *decorators) {
if (!decorators) {
return false;
}
for (int i = 0; decorators[i]; i++) {
const char *d = decorators[i];
/* Path-qualified async/param test macros (substring match, robust to the
* optional argument list and the surrounding #[ ]). */
if (strstr(d, "tokio::test") || strstr(d, "async_std::test") ||
strstr(d, "actix_rt::test") || strstr(d, "test_case::case")) {
return true;
}
/* Bare #[test] / #[test(...)]: match the bracketed path exactly so we do
* NOT match the unrelated #[test_case::case] (handled above) or a
* hypothetical #[test_crate]. */
if (strstr(d, "#[test]") || strstr(d, "#[test(")) {
return true;
}
}
return false;
}

static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn,
const char *const *decorators) {
if (!decorators) {
Expand Down Expand Up @@ -3192,6 +3221,7 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// predicate into the QN so both branches survive the graph upsert (#495).
if (ctx->language == CBM_LANG_RUST) {
def.qualified_name = rust_cfg_qualified_name(a, def.qualified_name, def.decorators);
def.is_test = rust_def_is_test(def.decorators);
}

// Docstring
Expand Down
41 changes: 41 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,46 @@ TEST(walk_defs_no_truncation_over_4096_issue668) {
* Suite
* ═══════════════════════════════════════════════════════════════════ */

/* Rust: inline #[test]/#[tokio::test] functions must be marked is_test so the
* store.c `is_test != 1` filter excludes them from graph context. Detection is
* otherwise file-path-based (cbm_is_test_file), so test fns in a regular .rs
* file leak. (#855) */
TEST(extract_rust_test_attr_marks_is_test_issue855) {
CBMFileResult *r = extract(
"pub fn real_fn() {}\n"
"\n"
"#[test]\n"
"fn sync_test() {}\n"
"\n"
"#[tokio::test]\n"
"async fn async_test() {}\n",
CBM_LANG_RUST, "t", "src/lib.rs");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);

int real = -1, sync = -1, asyn = -1;
for (int i = 0; i < r->defs.count; i++) {
const char *n = r->defs.items[i].name;
if (!n) {
continue;
}
if (strcmp(n, "real_fn") == 0) {
real = r->defs.items[i].is_test ? 1 : 0;
} else if (strcmp(n, "sync_test") == 0) {
sync = r->defs.items[i].is_test ? 1 : 0;
} else if (strcmp(n, "async_test") == 0) {
asyn = r->defs.items[i].is_test ? 1 : 0;
}
}
ASSERT(real >= 0 && sync >= 0 && asyn >= 0 && "all three fns extracted");
ASSERT(real == 0 && "real_fn is NOT a test");
ASSERT(sync == 1 && "#[test] fn is_test");
ASSERT(asyn == 1 && "#[tokio::test] fn is_test");

cbm_free_result(r);
PASS();
}

SUITE(extraction) {
/* Initialize extraction library */
cbm_init();
Expand Down Expand Up @@ -3597,6 +3637,7 @@ SUITE(extraction) {
RUN_TEST(complexity_go_method_receiver_self_recursion);
RUN_TEST(complexity_access_depth_and_params);
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);

cbm_shutdown();
}
Loading