From ad435fd05da75f9e751a202d229fc793100afbc6 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 10 Nov 2026 00:15:38 +0100 Subject: [PATCH] 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 --- src/pipeline/pass_pkgmap.c | 519 +++++++++++++++++++++++++++++++++++-- tests/test_pipeline.c | 165 ++++++++++++ 2 files changed, 669 insertions(+), 15 deletions(-) diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 7af626b2d..4c4c25ce8 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -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); } } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 9e6af1bbe..798406d73 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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);