Repository navigation
fix(rust): resolve use paths inside the importing crate - #2943
Merged
Merged
Conversation
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 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In a Cargo workspace, Rust
usedeclarations were bound to the wrong crate, and imports of external crates were bound to unrelated project symbols:use crate::util::helper;oruse super::proto::encode;in cratebetaboundalpha'sutil::helper/net::proto::encodewhenever both crates define the item, simply becausealphasorts first. On jdx/mise (22 packages) 720 Rust IMPORTS crossed into another workspace crate this way on main (1,768 with fix(rust): expand grouped use imports #2932), and 259 of 927 import_map CALLS followed them (528 of 2,303 with fix(rust): expand grouped use imports #2932).use serde_json::json;bound the project's ownjsonfunction (and everyjson!call through import_map),use meili_snap::snapshot;(a macro of a member crate) boundFileStore::snapshot,use std::env;in a build script bound a benchmark fixture'senv, and a path segment above the item (use crate::util::Gone;, noGonein the crate) bound a function namedutil. 1,123 such IMPORTS on mise main, 513 on meilisearch.Root cause
resolve_import_strategies(src/pipeline/pass_pkgmap.c) has no Rust branch:cbm_pipeline_resolve_module) never matches a::path, so every Rust import falls through to the name guesses.cbm_gbuf_find_by_name) and keeps the lexicographically smallest qualified name. Nothing restricts it to the importing crate, nothing tells an external crate from a workspace member, and an enclosing segment (a module name) may bind a function.crate::/self::/super::and resolves the rest from the repository root rather than the crate's module root, ignoring whereself/superpoint.Both pipeline drivers build the CALLS import map from these IMPORTS edges (
cbm_pxc_build_import_map), so import_map CALLS inherited every wrong target.Fix
For a Rust file under a Cargo.toml (the nearest one is its crate), an import is classified by its first path segment and resolved inside exactly one crate:
crate::is the importing crate's module root:src/(ortests/,benches/,examples/; a build script is its own root);self::/super::are relative to the importing file's module path (a/b.rsanda/b/mod.rsarea::b);mod foo;plususe foo::Bar;);foo-baris the cratefoo_bar) is that member;Inside that crate: the item at the module the path names wins; else the module the full path names (
use crate::cli;, globs); 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 (a segment above the leaf names a module or a type, never a function). Nothing else is guessed. Ties break on module-path fit, then qualified name, so the result is deterministic. Files with no Cargo.toml above them keep the generic strategies unchanged; other languages are untouched.No new tool, flag or environment variable; no new allocation sites (memory-core count unchanged); Strategy 4's prefix loop moved into a helper (
resolve_module_prefixes) that the crate tree reuses, with its old behaviour kept.Results
Release builds, full index, default workers unless noted. Corpora: jdx/mise v2026.11.2 (43ee02c5b497, 22 packages) and meilisearch 979167f13b5f (27 packages). "main+#2932" is a local merge of #2932's head 0df1074 used only for measurement; this branch is based on main.
jdx/mise
meilisearch
* classifier residue, checked by hand: imports of
r#match(a raw-identifier child module) and member imports through[dependencies.<name>]tables or a re-exported member crate, which the census script does not parse; the resolver binds them correctly."Into a crate the caller does not depend on" uses each crate's Cargo.toml dependency tables: such an edge is wrong by construction.
Judged sample. 54 changed edges (36 on mise, 18 on meilisearch), drawn with a fixed seed (20261109) across every class of change (IMPORTS retargeted / removed / added, import_map CALLS retargeted / removed / added, suffix_match retargets by crate membership, new suffix_match edges), each judged against the source: 28 better, 13 wrong before and after (mostly std methods such as
as_str,parent,Default::default), 12 worse, 1 unclear. Every IMPORTS and import_map change in the sample was better except the recall losses below. The worse ones are 6 suffix_match retargets and 2 new suffix_match edges (suffix_match ranks by import reachability, which moves with the corrected imports; its share of impossible edges still falls, see the tables) and 4 recall losses (next section). A further seeded check of the 822 import_map CALLS that main+#2932+fix drops relative to main+#2932 on meilisearch found 6 of 6 correct removals (allsnapshot!bound toFileStore::snapshot; 538 of the 822 are that macro).Determinism. Two runs of each fixed binary produce byte-identical graphs (whole-graph canonical dump) on both corpora; IMPORTS are identical between
CBM_WORKERS=1and the default. The sequential and parallel drivers already differ in Rust CALLS on main (mise: 1,070 / 944 edges only in one of them); with the fix 6 import_map edges join that existing gap on mise (correct calls through a now-resolved module import,ini_merge::merge(), that only the sequential driver uses).Control. helm 9b88efd6 (Go): the whole graph is byte-identical across all four binaries.
Known limits (follow-ups, not in this change)
src/file.rs: pub use mise_util::file::*;), souse crate::file::readno longer binds anything instead of bindingmise_util::file::readby name. At most 156 of the 720 relative cross-crate IMPORTS on mise main were right this way (0 of 146 on meilisearch). Followingpub use <member>::...::*would recover them.#[path = "..."]and items inside inlinemod x { ... }blocks are found only through the file path.unique_namefallback, which is not an import binding: on meilisearch 680snapshot!calls land onFileStore::snapshotthat way on main and with this change alike (the 69 import_map ones are gone).Tests
pipeline_rust_workspace_relative_import_stays_in_crate(+_parallel, which forces the parallel driver): a two-crate workspace wherecrate::util::helperandsuper::proto::encodemust bind their own crate although the sibling crate has the same paths,serde_json::json/json!and a build script'suse std::envmust bind nothing,crate::util::Gonemust not bind a functionutil, andalpha_core::util::helper(packagealpha-core) must still bind into alpha. Both fail on main.Refs #2922, #2932