fix(rust): resolve use paths inside the importing crate

In a Cargo workspace a relative `use` bound a sibling crate's item. The
import resolver has no Rust branch, so `crate::util::helper` fell through
to the project-wide symbol-name fallback (Strategy 3), which matches the
last path segment, then every enclosing segment, against every same-named
definition in the project and keeps the lexicographically smallest QN:
whichever crate sorts first. The same fallback bound imports of external
crates to project symbols by name (`use serde_json::json` -> the project's
own `json` function, and every `json!` call through import_map) and let a
module segment bind a function (`crate::util::Gone` -> fn `util`), while
Strategy 4 resolved `crate::`, `self::` and `super::` from the repository
root instead of the crate's module root.

A Rust import of a file under a Cargo.toml is now classified by its first
path segment and resolved inside exactly one crate:
- `crate::` is the importing crate's module root: the nearest Cargo.toml's
  src/ (or tests/, benches/, examples/; a build script is its own root);
- `self::` / `super::` are relative to the importing file's module path;
- a child module of the importing module is that module (2018 paths);
- a workspace member's crate name (a package `foo-bar` is `foo_bar`) is
  that member;
- anything else (std, core, a dependency) binds no project symbol.
Inside the crate the item at the module the path names wins, else the
module the full path names, else a same-named item in a submodule of that
module (a re-export from below), else the type an associated item or enum
variant belongs to. Nothing else is guessed, ties break on module-path fit
then QN, and files with no Cargo.toml above them keep the generic
strategies. The CALLS import map is built from these IMPORTS edges in both
drivers, so import_map CALLS follow.

On jdx/mise this removes all 720 relative IMPORTS that crossed into
another workspace crate, all 1,123 IMPORTS bound through an external-crate
path and all 259 import_map CALLS that followed them; IMPORTS and
import_map CALLS into a crate the caller does not depend on drop from
920 and 12 to 0. Wall time and index worker peak RSS are unchanged; a
non-Rust control graph is byte-identical.

Strategy 4's module-prefix loop moves into resolve_module_prefixes so the
crate tree can reuse its lookup (plus the mod.rs form); Strategy 4's own
behaviour is unchanged.

Refs #2922, #2932

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-11-10 00:15:38 +01:00
parent 6df7fcb05d
commit ad435fd05d
2 changed files with 669 additions and 15 deletions
+504 -15
View File
@@ -2367,6 +2367,494 @@ static const cbm_gbuf_node_t *resolve_kustomize_entry(const cbm_pipeline_ctx_t *
return found;
}
/* The first import-targetable node the module path in `work` names, trying
* successively shorter parent paths while a cut keeps at least `keep` bytes
* (Strategy 4 below, and the Rust crate tree). rust_tree: `work` is a
* repository path inside a Rust crate, naming work.rs or work/mod.rs
* directly, so no module resolver is involved. */
static const cbm_gbuf_node_t *resolve_module_prefixes(const cbm_pipeline_ctx_t *ctx,
const char *source_rel,
const char *source_file_qn, char *work,
size_t keep, bool rust_tree, bool php_only) {
for (;;) {
for (int form = 0; form < (rust_tree ? PAIR_LEN : SKIP_ONE); form++) {
char mod_rs[PKGMAP_PATH_BUF];
int len = form ? snprintf(mod_rs, sizeof(mod_rs), "%s/mod", work) : 0;
if (len < 0 || (size_t)len >= sizeof(mod_rs)) {
continue;
}
const char *path = form ? mod_rs : work;
char *rqn = rust_tree ? cbm_pipeline_fqn_module(ctx->project_name, path)
: cbm_pipeline_resolve_module(ctx, source_rel, path);
const cbm_gbuf_node_t *n = rqn ? cbm_gbuf_find_by_qn(ctx->gbuf, rqn) : NULL;
free(rqn);
if (n && (!php_only || node_in_php_file(n)) && import_targetable_label(n->label) &&
(!source_file_qn || !n->qualified_name ||
strcmp(n->qualified_name, source_file_qn) != 0)) {
return n;
}
}
char *sl = strrchr(work, '/');
if (!sl || (size_t)(sl - work) < keep) {
return NULL;
}
*sl = '\0';
}
}
/* ── Rust `use` paths resolve inside one crate ─────────────────────── */
/* A Rust `use` path names an item of exactly one crate: `crate::` is the
* importing crate's root module, `self::`/`super::` are relative to the
* importing file's module, a workspace member's crate name is that member, a
* child module of the importing module is that module, and anything else
* (std, core, a dependency) is outside the repository. The generic strategies
* below instead matched the last segment against every same-named definition
* in the project and took the smallest QN: in a Cargo workspace,
* `crate::util::helper` bound a SIBLING crate's util::helper, and
* `serde_json::json` bound the project's own `json` function. The crate of a
* file is its nearest Cargo.toml; without one the generic strategies stay. */
enum {
RUST_USE_MAX_SEGS = 64,
RUST_USE_CANDS = 8,
RUST_RS_EXT_LEN = 3, /* strlen(".rs") */
RUST_MOD_TAIL_LEN = 4, /* strlen("/mod") */
RUST_KEYWORD_LEN = 3, /* strlen("pub") == strlen("use") */
RUST_SELF_LEN = 4, /* strlen("self") */
RUST_NON_ASCII_MIN = 0x80, /* identifiers admit any byte >= 0x80 */
};
typedef enum {
RUST_USE_UNSCOPED = 0, /* no crate known for the importer: generic strategies */
RUST_USE_NO_TARGET, /* an external crate, or a path above the crate root */
RUST_USE_SCOPED, /* resolved inside one crate's module tree */
} rust_use_kind_t;
typedef struct {
char buf[PKGMAP_PATH_BUF]; /* the segments, NUL-separated */
const char *seg[RUST_USE_MAX_SEGS];
int n;
bool glob; /* trailing `::*` */
bool global; /* leading `::`: an extern crate */
} rust_path_t;
typedef struct {
const char *name; /* a written segment of the path */
size_t mod_len; /* bytes of the path naming the modules above it */
int nmod; /* segments in those bytes */
} rust_cand_t;
typedef struct {
rust_path_t p;
char root[PKGMAP_PATH_BUF]; /* module root of the crate target ("crates/x/src") */
char path[PKGMAP_PATH_BUF]; /* module path inside root, '/'-joined, written segments last */
bool has_leaf; /* cands[0] is the named item (no glob) */
rust_cand_t cands[RUST_USE_CANDS]; /* the written segments, innermost first */
int ncands;
} rust_use_t;
static bool rust_ident_byte(unsigned char c) {
return isalnum(c) || c == '_' || c >= RUST_NON_ASCII_MIN;
}
static const char *rust_skip_ws(const char *p) {
while (*p && isspace((unsigned char)*p)) {
p++;
}
return p;
}
/* Skip a `pub`, `pub(crate)`, `pub(in path)` visibility and a `use` keyword
* that an import's text may still carry. */
static const char *rust_skip_use_prefix(const char *p) {
p = rust_skip_ws(p);
if (strncmp(p, "pub", RUST_KEYWORD_LEN) == 0 &&
!rust_ident_byte((unsigned char)p[RUST_KEYWORD_LEN])) {
p = rust_skip_ws(p + RUST_KEYWORD_LEN);
if (*p == '(') {
const char *close = strchr(p, ')');
p = close ? rust_skip_ws(close + SKIP_ONE) : p;
}
}
if (strncmp(p, "use", RUST_KEYWORD_LEN) == 0 && isspace((unsigned char)p[RUST_KEYWORD_LEN])) {
p = rust_skip_ws(p + RUST_KEYWORD_LEN);
}
return p;
}
/* The written path of an import as text: a group `a::{B, C}` stands for its
* first member (as import_candidate_symbol does), ` as alias` is dropped. */
static bool rust_path_text(const char *module_path, char *out, size_t outsz) {
const char *p = rust_skip_use_prefix(module_path);
const char *brace = strchr(p, '{');
int n;
if (!brace) {
n = snprintf(out, outsz, "%s", p);
} else {
const char *m = rust_skip_ws(brace + SKIP_ONE);
size_t mlen = strcspn(m, ",}{");
while (mlen > 0 &&
(isspace((unsigned char)m[mlen - SKIP_ONE]) || m[mlen - SKIP_ONE] == ':')) {
mlen--;
}
bool self_member = mlen == RUST_SELF_LEN && strncmp(m, "self", RUST_SELF_LEN) == 0;
if (mlen == 0 || self_member) {
n = snprintf(out, outsz, "%.*s", (int)(brace - p), p);
} else {
n = snprintf(out, outsz, "%.*s%.*s", (int)(brace - p), p, (int)mlen, m);
}
}
if (n < 0 || (size_t)n >= outsz) {
return false;
}
char *as = strstr(out, " as ");
if (as) {
*as = '\0';
}
return true;
}
/* Split a Rust use path into its segments. False when the text is no plain
* path (an unparsed form keeps the generic strategies). */
static bool rust_path_split(const char *module_path, rust_path_t *out) {
memset(out, 0, sizeof(*out));
if (!module_path || !rust_path_text(module_path, out->buf, sizeof(out->buf))) {
return false;
}
char *p = (char *)rust_skip_ws(out->buf);
size_t len = strlen(p);
while (len > 0 && (isspace((unsigned char)p[len - SKIP_ONE]) || p[len - SKIP_ONE] == ':')) {
p[--len] = '\0';
}
if (len > 0 && p[len - SKIP_ONE] == '*') {
out->glob = true;
p[--len] = '\0';
while (len > 0 && (isspace((unsigned char)p[len - SKIP_ONE]) || p[len - SKIP_ONE] == ':')) {
p[--len] = '\0';
}
}
if (p[0] == ':' && p[SKIP_ONE] == ':') {
out->global = true;
p += PAIR_LEN;
}
while (*p) {
p = (char *)rust_skip_ws(p);
if (strncmp(p, "r#", PAIR_LEN) == 0) {
p += PAIR_LEN;
}
char *start = p;
while (rust_ident_byte((unsigned char)*p)) {
p++;
}
char *end = p;
p = (char *)rust_skip_ws(p);
if (end == start || out->n >= RUST_USE_MAX_SEGS) {
return false;
}
if (*p == ':' && p[SKIP_ONE] == ':') {
p += PAIR_LEN;
} else if (*p) {
return false;
}
*end = '\0';
out->seg[out->n++] = start;
}
return out->n > 0;
}
/* The directory of the nearest Cargo.toml above source_rel ("" = the
* repository root). False when no manifest is indexed above the file. */
static bool rust_crate_dir(const cbm_pipeline_ctx_t *ctx, const char *source_rel, char *out,
size_t outsz) {
int n = snprintf(out, outsz, "%s", source_rel);
if (n < 0 || (size_t)n >= outsz) {
return false;
}
for (;;) {
char *slash = strrchr(out, '/');
if (slash) {
*slash = '\0';
} else {
out[0] = '\0';
}
char manifest[PKGMAP_PATH_BUF];
n = snprintf(manifest, sizeof(manifest), "%s%sCargo.toml", out, out[0] ? "/" : "");
if (n > 0 && (size_t)n < sizeof(manifest) &&
cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, manifest)) {
return true;
}
if (!slash) {
return false;
}
}
}
/* The module root of the crate target holding source_rel in the package at
* crate_dir: the package's first directory on the way down (src/ for the
* library and binaries, tests/, benches/, examples/), or crate_dir itself
* for a file directly in it (build.rs, a crate root: *is_root = true). */
static bool rust_module_root(const char *source_rel, const char *crate_dir, char *out, size_t outsz,
bool *is_root) {
size_t dl = strlen(crate_dir);
const char *slash = strchr(source_rel + (dl ? dl + SKIP_ONE : 0), '/');
size_t n = slash ? (size_t)(slash - source_rel) : dl;
if (n >= outsz) {
return false;
}
memcpy(out, source_rel, n);
out[n] = '\0';
*is_root = !slash;
return true;
}
/* root/rel, or rel when root is the repository root (""). */
static bool rust_join(char *out, size_t outsz, const char *root, const char *rel) {
int n = snprintf(out, outsz, "%s%s%s", root, root[0] ? "/" : "", rel);
return n > 0 && (size_t)n < outsz;
}
/* The module path of the Rust file at path inside root, '/'-joined:
* root/a/b.rs and root/a/b/mod.rs are "a/b", root/lib.rs and root/main.rs
* are "". False when path is no .rs file under root. */
static bool rust_file_module(const char *path, const char *root, char *out, size_t outsz) {
size_t rl = strlen(root);
if (!path || (rl && (strncmp(path, root, rl) != 0 || path[rl] != '/'))) {
return false;
}
const char *rel = path + (rl ? rl + SKIP_ONE : 0);
size_t n = strlen(rel);
if (n <= RUST_RS_EXT_LEN || strcmp(rel + n - RUST_RS_EXT_LEN, ".rs") != 0 ||
n - RUST_RS_EXT_LEN >= outsz) {
return false;
}
n -= RUST_RS_EXT_LEN;
memcpy(out, rel, n);
out[n] = '\0';
if (strcmp(out, "lib") == 0 || strcmp(out, "main") == 0 || strcmp(out, "mod") == 0) {
out[0] = '\0';
} else if (n > RUST_MOD_TAIL_LEN && strcmp(out + n - RUST_MOD_TAIL_LEN, "/mod") == 0) {
out[n - RUST_MOD_TAIL_LEN] = '\0';
}
return true;
}
/* The module root of the workspace member whose crate name is `name` (a
* Cargo package name spells '_' as '-' too). False when no member of that
* name has a Rust library root in the graph. */
static bool rust_member_root(const cbm_pipeline_ctx_t *ctx, const char *name, char *out,
size_t outsz) {
CBMHashTable *pkgmap = cbm_pipeline_get_pkgmap();
if (!pkgmap) {
return false;
}
const char *qn = (const char *)cbm_ht_get(pkgmap, name);
char dashed[CBM_SZ_256];
int dn = qn || !strchr(name, '_') ? -1 : snprintf(dashed, sizeof(dashed), "%s", name);
if (dn > 0 && (size_t)dn < sizeof(dashed)) {
for (char *c = dashed; *c; c++) {
*c = *c == '_' ? '-' : *c;
}
qn = (const char *)cbm_ht_get(pkgmap, dashed);
}
const cbm_gbuf_node_t *lib = qn ? cbm_gbuf_find_by_qn(ctx->gbuf, qn) : NULL;
const char *fp = lib ? lib->file_path : NULL;
const char *slash = fp ? strrchr(fp, '/') : NULL;
if (!slash || !ends_with(fp, ".rs") || (size_t)(slash - fp) >= outsz) {
return false;
}
memcpy(out, fp, (size_t)(slash - fp));
out[slash - fp] = '\0';
return true;
}
/* Whether `name` is a child module of the module at root/own: a file
* root/own/name.rs or root/own/name/mod.rs is indexed. */
static bool rust_child_module(const cbm_pipeline_ctx_t *ctx, const char *root, const char *own,
const char *name) {
static const char *const tails[] = {".rs", "/mod.rs"};
char dir[PKGMAP_PATH_BUF];
if (!rust_join(dir, sizeof(dir), root, own)) {
return false;
}
for (size_t i = 0; i < sizeof(tails) / sizeof(tails[0]); i++) {
char rel[PKGMAP_PATH_BUF];
int n = snprintf(rel, sizeof(rel), "%s%s%s%s", dir, dir[0] ? "/" : "", name, tails[i]);
if (n > 0 && (size_t)n < sizeof(rel) &&
cbm_pipeline_file_node(ctx->gbuf, ctx->project_name, rel)) {
return true;
}
}
return false;
}
/* Append the segments seg[from..n) to u->path and record each one with the
* module path above it. False when nothing is named. */
static bool rust_use_finish(rust_use_t *u, int from) {
if (!u->p.glob && from >= u->p.n) {
return false; /* `use super;`, `use crate::{self}`: no item */
}
size_t len = strlen(u->path);
int nseg = len ? SKIP_ONE : 0;
for (size_t i = 0; i < len; i++) {
nseg += u->path[i] == '/';
}
rust_cand_t seen[RUST_USE_MAX_SEGS];
int nseen = 0;
for (int i = from; i < u->p.n; i++) {
seen[nseen++] = (rust_cand_t){u->p.seg[i], len, nseg++};
int n = snprintf(u->path + len, sizeof(u->path) - len, "%s%s", len ? "/" : "", u->p.seg[i]);
if (n < 0 || (size_t)n >= sizeof(u->path) - len) {
return false;
}
len += (size_t)n;
}
u->has_leaf = !u->p.glob;
u->ncands = 0;
for (int i = nseen - SKIP_ONE; i >= 0 && u->ncands < RUST_USE_CANDS; i--) {
u->cands[u->ncands++] = seen[i];
}
return true;
}
/* Classify one Rust import of source_rel and, for SCOPED, fill *u. */
static rust_use_kind_t rust_use_scope(const cbm_pipeline_ctx_t *ctx, const char *source_rel,
const char *module_path, rust_use_t *u) {
char crate_dir[PKGMAP_PATH_BUF];
char own[PKGMAP_PATH_BUF] = "";
bool is_root = false;
if (!source_rel || !rust_path_split(module_path, &u->p) ||
!rust_crate_dir(ctx, source_rel, crate_dir, sizeof(crate_dir)) ||
!rust_module_root(source_rel, crate_dir, u->root, sizeof(u->root), &is_root) ||
(!is_root && !rust_file_module(source_rel, u->root, own, sizeof(own)))) {
return RUST_USE_UNSCOPED;
}
u->path[0] = '\0';
u->ncands = 0;
const char *head = u->p.seg[0];
int from = SKIP_ONE;
if (!u->p.global && strcmp(head, "crate") == 0) {
/* the crate root: path starts empty */
} else if (!u->p.global && strcmp(head, "self") == 0) {
snprintf(u->path, sizeof(u->path), "%s", own);
} else if (!u->p.global && strcmp(head, "super") == 0) {
snprintf(u->path, sizeof(u->path), "%s", own);
for (from = 0; from < u->p.n && strcmp(u->p.seg[from], "super") == 0; from++) {
if (!u->path[0]) {
return RUST_USE_NO_TARGET; /* above the crate root */
}
char *up = strrchr(u->path, '/');
*(up ? up : u->path) = '\0';
}
} else if (!u->p.global && rust_child_module(ctx, u->root, own, head)) {
snprintf(u->path, sizeof(u->path), "%s", own);
from = 0;
} else if (!rust_member_root(ctx, head, u->root, sizeof(u->root))) {
return RUST_USE_NO_TARGET; /* std, core, a dependency */
}
return rust_use_finish(u, from) ? RUST_USE_SCOPED : RUST_USE_NO_TARGET;
}
/* How well a definition in the module cand_mod fits the module path
* want[0..want_len): leading segments in common, and segments it adds. */
static void rust_module_fit(const char *cand_mod, const char *want, size_t want_len, int *common,
int *extra) {
*common = 0;
*extra = 0;
const char *a = cand_mod;
size_t bi = 0;
bool diverged = false;
while (*a) {
size_t al = strcspn(a, "/");
size_t bl = 0;
while (bi + bl < want_len && want[bi + bl] != '/') {
bl++;
}
if (!diverged && bi < want_len && al == bl && strncmp(a, want + bi, al) == 0) {
(*common)++;
bi += bl + (bi + bl < want_len ? SKIP_ONE : 0);
} else {
diverged = true;
(*extra)++;
}
a += al + (a[al] ? SKIP_ONE : 0);
}
}
typedef struct {
const cbm_gbuf_node_t *node;
int extra; /* module segments below the named module */
} rust_pick_t;
/* The definition named c->name inside the module subtree the path names above
* it (the module itself or a submodule that re-exports through it): fewest
* extra segments, then smallest QN. type_only: a segment before the last
* names a module or a type, never a function. */
static rust_pick_t rust_use_pick(const cbm_pipeline_ctx_t *ctx, const char *source_file_qn,
const rust_use_t *u, const rust_cand_t *c, bool type_only) {
rust_pick_t best = {NULL, 0};
const cbm_gbuf_node_t **hits = NULL;
int n = 0;
if (cbm_gbuf_find_by_name(ctx->gbuf, c->name, &hits, &n) != 0 || !hits) {
return best;
}
for (int i = 0; i < n; i++) {
const cbm_gbuf_node_t *cand = hits[i];
char cand_mod[PKGMAP_PATH_BUF];
if (!cand || !cand->qualified_name || !import_targetable_label(cand->label) ||
(type_only && !cbm_label_is_type_like(cand->label)) ||
cbm_node_props_generated(cand->properties_json) ||
(source_file_qn && strcmp(cand->qualified_name, source_file_qn) == 0) ||
!rust_file_module(cand->file_path, u->root, cand_mod, sizeof(cand_mod))) {
continue;
}
int common = 0;
int extra = 0;
rust_module_fit(cand_mod, u->path, c->mod_len, &common, &extra);
if (common == c->nmod && (!best.node || extra < best.extra ||
(extra == best.extra &&
strcmp(cand->qualified_name, best.node->qualified_name) < 0))) {
best = (rust_pick_t){cand, extra};
}
}
return best;
}
/* Resolve a SCOPED Rust import inside u->root, never outside it: the item at
* its module, else the module the full path names (`use crate::a::b;`, a
* glob), else the item re-exported from a submodule of its module, else the
* type an associated item or enum variant belongs to. Nothing else: an item
* the crate re-exports from another crate is no item of this crate. */
static const cbm_gbuf_node_t *rust_use_resolve(const cbm_pipeline_ctx_t *ctx,
const char *source_rel, const char *source_file_qn,
const rust_use_t *u) {
rust_pick_t leaf = {NULL, 0};
if (u->has_leaf) {
leaf = rust_use_pick(ctx, source_file_qn, u, &u->cands[0], false);
if (leaf.node && leaf.extra == 0) {
return leaf.node;
}
}
char work[PKGMAP_PATH_BUF];
if (u->path[0] && rust_join(work, sizeof(work), u->root, u->path)) {
const cbm_gbuf_node_t *mod = resolve_module_prefixes(ctx, source_rel, source_file_qn, work,
strlen(work), true, false);
if (mod) {
return mod;
}
}
if (leaf.node) {
return leaf.node;
}
for (int i = u->has_leaf ? SKIP_ONE : 0; i < u->ncands; i++) {
rust_pick_t owner = rust_use_pick(ctx, source_file_qn, u, &u->cands[i], true);
if (owner.node) {
return owner.node;
}
}
return NULL;
}
/* Language-generic strategies retain their ordering. PHP name imports with
* no namespace information (and global uses) restrict candidates before any
* winner is selected, so a non-PHP collision cannot hide a later PHP hit.
@@ -2414,6 +2902,20 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
return target;
}
/* Rust: a `use` path names an item of one crate, never a same-named
* definition of another crate (see rust_use_scope). */
if (src_lang == CBM_LANG_RUST && !php_only) {
rust_use_t ru;
switch (rust_use_scope(ctx, source_rel, imp->module_path, &ru)) {
case RUST_USE_NO_TARGET:
return NULL;
case RUST_USE_SCOPED:
return rust_use_resolve(ctx, source_rel, source_file_qn, &ru);
case RUST_USE_UNSCOPED:
break;
}
}
/* Name-guess fallbacks below (Strategy 1b sibling-file, Strategy 3
* symbol-name) are gated per importing-file language — see
* cbm_import_symbol_fallback_allowed (#1934). */
@@ -2680,21 +3182,8 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
if (body[0]) {
char work[1024];
snprintf(work, sizeof(work), "%s", body);
for (;;) {
char *rqn = cbm_pipeline_resolve_module(ctx, source_rel, work);
const cbm_gbuf_node_t *n = rqn ? cbm_gbuf_find_by_qn(ctx->gbuf, rqn) : NULL;
free(rqn);
if (n && (!php_only || node_in_php_file(n)) && import_targetable_label(n->label) &&
(!source_file_qn || !n->qualified_name ||
strcmp(n->qualified_name, source_file_qn) != 0)) {
return n;
}
char *sl = strrchr(work, '/');
if (!sl) {
break;
}
*sl = '\0';
}
return resolve_module_prefixes(ctx, source_rel, source_file_qn, work, 0, false,
php_only);
}
}
+165
View File
@@ -13185,6 +13185,169 @@ TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel) {
return run_rust_std_receiver_probe(52, "par");
}
/* IMPORTS edges from the file at src_file whose local_name is local and whose
* target node lives in target_file (any target when NULL). -1 on a query
* failure. */
static int rust_import_into_file_count(cbm_store_t *s, const char *project, const char *src_file,
const char *local, const char *target_file) {
static const char sql[] =
"SELECT count(*) FROM edges e JOIN nodes f ON f.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='IMPORTS' "
"AND f.file_path=?2 AND json_extract(e.properties,'$.local_name')=?3 "
"AND (?4 IS NULL OR t.file_path=?4)";
sqlite3 *db = cbm_store_get_db(s);
sqlite3_stmt *st = NULL;
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
sqlite3_finalize(st);
return -1;
}
int count = -1;
if (sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_bind_text(st, 2, src_file, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_bind_text(st, 3, local, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
(target_file ? sqlite3_bind_text(st, 4, target_file, -1, SQLITE_TRANSIENT)
: sqlite3_bind_null(st, 4)) == SQLITE_OK &&
sqlite3_step(st) == SQLITE_ROW) {
count = sqlite3_column_int(st, 0);
}
sqlite3_finalize(st);
return count;
}
/* CALLS edges from a node named src_name to a node named tgt_name carrying the
* given resolution strategy. -1 on a query failure. */
static int rust_calls_by_strategy_count(cbm_store_t *s, const char *project, const char *src_name,
const char *tgt_name, const char *strategy) {
static const char sql[] =
"SELECT count(*) FROM edges e JOIN nodes f ON f.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='CALLS' "
"AND f.name=?2 AND t.name=?3 AND json_extract(e.properties,'$.strategy')=?4";
sqlite3 *db = cbm_store_get_db(s);
sqlite3_stmt *st = NULL;
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
sqlite3_finalize(st);
return -1;
}
int count = -1;
if (sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_bind_text(st, 2, src_name, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_bind_text(st, 3, tgt_name, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_bind_text(st, 4, strategy, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_step(st) == SQLITE_ROW) {
count = sqlite3_column_int(st, 0);
}
sqlite3_finalize(st);
return count;
}
/* A Cargo workspace of two crates that both define util::helper and
* net::proto::encode. A relative `use` names a path inside the importing
* crate, so `crate::util::helper` and `super::proto::encode` from beta bind
* beta's items, never alpha's (the project-wide name fallback took the
* lexicographically smallest QN, i.e. always alpha). `serde_json::json` names
* an external crate: no project symbol may stand in for it, so alpha's `json`
* function gets neither the IMPORTS edge nor the import_map CALLS of `json!`
* (nor alpha's `env` the `use std::env` of beta's build script, a crate root),
* and `crate::util::Gone`, an item beta does not define, binds nothing: its
* `util` segment names a module, never alpha's function `util`.
* `alpha_core::util::helper` names a workspace member (package `alpha-core`)
* and still binds into alpha. pad_files > 0 forces the parallel resolver. */
static int rust_workspace_relative_import_case(int pad_files) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_ws_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "Cargo.toml",
"[workspace]\nmembers = [\"crates/alpha\", \"crates/beta\"]\n");
write_temp_file(tmp, "crates/alpha/Cargo.toml",
"[package]\nname = \"alpha-core\"\nversion = \"0.1.0\"\n");
write_temp_file(tmp, "crates/alpha/src/lib.rs",
"pub mod net;\npub mod util;\n\npub fn json() -> u32 {\n 7\n}\n\n"
"pub fn env() -> u32 {\n 8\n}\n\npub fn util() -> u32 {\n 9\n}\n");
write_temp_file(tmp, "crates/alpha/src/util.rs", "pub fn helper() -> u32 {\n 1\n}\n");
write_temp_file(tmp, "crates/alpha/src/net/mod.rs", "pub mod proto;\n");
write_temp_file(tmp, "crates/alpha/src/net/proto.rs", "pub fn encode() -> u32 {\n 10\n}\n");
write_temp_file(tmp, "crates/beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"0.1.0\"\n");
write_temp_file(tmp, "crates/beta/src/lib.rs",
"pub mod bridge;\npub mod net;\npub mod run;\npub mod util;\n");
write_temp_file(tmp, "crates/beta/src/util.rs", "pub fn helper() -> u32 {\n 2\n}\n");
write_temp_file(tmp, "crates/beta/src/net/mod.rs", "pub mod client;\npub mod proto;\n");
write_temp_file(tmp, "crates/beta/src/net/proto.rs", "pub fn encode() -> u32 {\n 20\n}\n");
write_temp_file(tmp, "crates/beta/src/net/client.rs",
"use super::proto::encode;\n"
"\n"
"pub fn send() -> u32 {\n"
" encode()\n"
"}\n");
write_temp_file(tmp, "crates/beta/src/run.rs",
"use crate::util::helper;\n"
"use crate::util::Gone;\n"
"use serde_json::json;\n"
"\n"
"pub fn go() -> u32 {\n"
" helper()\n"
"}\n"
"\n"
"pub fn emit() -> u32 {\n"
" let _v = json!({\"a\": 1});\n"
" 0\n"
"}\n");
write_temp_file(tmp, "crates/beta/build.rs",
"use std::env;\n\nfn main() {\n let _ = env::var(\"OUT_DIR\");\n}\n");
write_temp_file(tmp, "crates/beta/src/bridge.rs",
"use alpha_core::util::helper;\n"
"\n"
"pub fn cross() -> u32 {\n"
" helper()\n"
"}\n");
cbm_pipeline_t *p = NULL;
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_ws.db", &p);
ASSERT_NOT_NULL(s);
const char *project = cbm_pipeline_project_name(p);
static const char run_rs[] = "crates/beta/src/run.rs";
static const char client_rs[] = "crates/beta/src/net/client.rs";
static const char bridge_rs[] = "crates/beta/src/bridge.rs";
static const char beta_util[] = "crates/beta/src/util.rs";
static const char alpha_util[] = "crates/alpha/src/util.rs";
static const char beta_proto[] = "crates/beta/src/net/proto.rs";
static const char alpha_proto[] = "crates/alpha/src/net/proto.rs";
/* crate:: stays in beta. */
ASSERT_EQ(rust_import_into_file_count(s, project, run_rs, "helper", beta_util), 1);
ASSERT_EQ(rust_import_into_file_count(s, project, run_rs, "helper", alpha_util), 0);
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "go", "helper", beta_util), 1);
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "go", "helper", alpha_util), 0);
/* super:: is relative to net::client inside beta. */
ASSERT_EQ(rust_import_into_file_count(s, project, client_rs, "encode", beta_proto), 1);
ASSERT_EQ(rust_import_into_file_count(s, project, client_rs, "encode", alpha_proto), 0);
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "send", "encode", beta_proto), 1);
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "send", "encode", alpha_proto), 0);
/* An external crate's macro is no project symbol. */
ASSERT_EQ(rust_import_into_file_count(s, project, run_rs, "json", NULL), 0);
ASSERT_EQ(rust_calls_by_strategy_count(s, project, "emit", "json", "import_map"), 0);
ASSERT_EQ(rust_import_into_file_count(s, project, "crates/beta/build.rs", "env", NULL), 0);
/* A path segment above the item names a module, never alpha's fn util. */
ASSERT_EQ(rust_import_into_file_count(s, project, run_rs, "Gone", NULL), 0);
/* A workspace member named by its crate name binds into that member. */
ASSERT_EQ(rust_import_into_file_count(s, project, bridge_rs, "helper", alpha_util), 1);
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "cross", "helper", alpha_util), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
TEST(pipeline_rust_workspace_relative_import_stays_in_crate) {
return rust_workspace_relative_import_case(0);
}
TEST(pipeline_rust_workspace_relative_import_stays_in_crate_parallel) {
return rust_workspace_relative_import_case(52);
}
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name);
/* Fixture for the #1942 bare-reference-vs-Field probes: a Go struct field
@@ -32072,6 +32235,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_rust_generic_impl_method_sources_its_calls_parallel);
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method);
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
RUN_TEST(pipeline_rust_workspace_relative_import_stays_in_crate);
RUN_TEST(pipeline_rust_workspace_relative_import_stays_in_crate_parallel);
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
RUN_TEST(pipeline_c_bare_ref_never_binds_field);