1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//! Pre-computed inputs shared across the per-package and workspace passes:
//! the parsed source files, which ones are reachable from a module tree, and
//! the dependency-name maps the analyzer needs to translate import names back
//! to package names.
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use cargo_metadata::{Metadata, Package, Target};
use ignore::{DirEntry, WalkBuilder};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{manifest::Manifest, source_parser::ParsedSource, util::read_to_string};
/// Workspace-wide state computed once and shared by every per-package run.
pub struct WorkspaceContext {
/// Absolute path to the workspace root directory.
pub root: PathBuf,
/// Absolute path to the workspace `Cargo.toml`.
pub manifest_path: PathBuf,
/// Raw `Cargo.toml` content (kept so renderers can show source spans).
pub manifest_content: String,
/// Parsed workspace manifest.
pub manifest: Manifest,
/// All Rust source files in the workspace, keyed by absolute path.
pub files: FxHashMap<PathBuf, ParsedSource>,
/// Files reachable from at least one entry point's module tree.
pub linked: FxHashSet<PathBuf>,
/// Absolute directories of every workspace member.
pub packages: FxHashSet<PathBuf>,
/// `[workspace.dependencies]` key → underlying package name (handles `package = "..."`).
pub dep_to_pkg: FxHashMap<String, String>,
/// `[workspace.metadata.cargo-shear].ignored` entries (raw dependency keys).
pub ignored_deps: FxHashSet<String>,
}
impl WorkspaceContext {
pub fn new(metadata: &Metadata) -> Result<Self> {
let root = metadata.workspace_root.as_std_path().to_path_buf();
let manifest_path = root.join("Cargo.toml");
let manifest_content = read_to_string(&manifest_path)?;
let manifest: Manifest = toml::from_str(&manifest_content)?;
let packages: FxHashSet<PathBuf> = metadata
.workspace_packages()
.iter()
.filter_map(|pkg| pkg.manifest_path.parent())
.map(|path| path.as_std_path().to_path_buf())
.collect();
let entry_points: FxHashSet<PathBuf> = metadata
.workspace_packages()
.iter()
.flat_map(|pkg| pkg.targets.iter())
.map(|target| target.src_path.as_std_path().to_path_buf())
.collect();
// Walk from each entry point's parent directory, not from the package root —
// some packages put `lib.rs` outside `src/`, and we want to follow the actual layout.
let parents: FxHashSet<PathBuf> =
entry_points.iter().filter_map(|path| path.parent()).map(Path::to_path_buf).collect();
let walked: FxHashSet<PathBuf> = parents
.into_par_iter()
.flat_map_iter(|parent| {
let packages = packages.clone();
WalkBuilder::new(&parent)
// Don't descend into directories that are themselves Cargo packages
// (each member is walked from its own entry points). We *do* still
// visit the package root itself — packages.contains(path) lets the
// current member's root through.
.filter_entry(move |entry| {
if let Some(file_type) = entry.file_type()
&& file_type.is_dir()
{
let path = entry.path();
if path.join("Cargo.toml").exists() {
return packages.contains(path);
}
}
true
})
.build()
.filter_map(Result::ok)
// Only `.rs` files; the walker also yields directories and other extensions.
.filter(|entry| {
entry.file_type().is_some_and(|file_type| file_type.is_file())
&& entry.path().extension().is_some_and(|extension| extension == "rs")
})
.map(DirEntry::into_path)
})
.collect();
// `WalkBuilder` was started from parent directories, which misses single-file
// entry points whose parent isn't itself walked (e.g. `build.rs` at the package root).
let paths: FxHashSet<PathBuf> =
entry_points.iter().filter(|path| path.is_file()).cloned().chain(walked).collect();
let files: FxHashMap<PathBuf, ParsedSource> = paths
.into_par_iter()
.filter_map(|path| {
let is_entry_point = entry_points.contains(&path);
ParsedSource::from_path(&path, is_entry_point).ok().map(|source| (path, source))
})
.collect();
let linked: FxHashSet<PathBuf> = entry_points
.into_iter()
.chain(files.values().flat_map(|parsed| {
parsed.paths.iter().filter_map(|path| {
// Only canonicalize when the path is relative (`./`, `../`); otherwise
// skip the syscall — the file may not exist on disk yet.
if path.as_os_str().as_encoded_bytes().starts_with(b".") {
path.canonicalize().ok()
} else {
Some(path.clone())
}
})
}))
.collect();
let dep_to_pkg = manifest
.workspace
.dependencies
.iter()
.map(|(dep, dependency)| {
let dep = dep.get_ref();
let pkg = dependency.get_ref().package().unwrap_or(dep);
(dep.to_owned(), pkg.to_owned())
})
.collect();
let ignored_deps = manifest
.workspace
.metadata
.cargo_shear
.ignored
.iter()
.map(|ignore| ignore.get_ref().clone())
.collect();
Ok(Self {
root,
manifest_path,
manifest_content,
manifest,
files,
linked,
packages,
dep_to_pkg,
ignored_deps,
})
}
}
/// Per-package state derived from `cargo metadata` plus the package's own manifest.
pub struct PackageContext<'a> {
/// Shared workspace state (source file map, ignored deps, ...).
pub workspace: &'a WorkspaceContext,
/// Package name as declared in `[package].name`.
pub name: String,
/// Absolute path to the package's directory (parent of its `Cargo.toml`).
pub directory: PathBuf,
/// Absolute path to the package's `Cargo.toml`.
pub manifest_path: PathBuf,
/// Raw manifest content (kept so renderers can show source spans).
pub manifest_content: String,
/// Parsed package manifest.
pub manifest: Manifest,
/// Cargo targets declared by this package (lib, bin, test, ...).
pub targets: Vec<Target>,
/// Import name → package name (e.g. `tokio_util` → `tokio-util`).
pub import_to_pkg: FxHashMap<String, String>,
/// Package name → import name (inverse of `import_to_pkg`).
pub pkg_to_import: FxHashMap<String, String>,
/// Union of package- and workspace-level `ignored = [...]` entries, normalised to import names.
pub ignored_imports: FxHashSet<String>,
}
impl<'a> PackageContext<'a> {
pub fn new(
workspace: &'a WorkspaceContext,
package: &Package,
metadata: &Metadata,
) -> Result<Self> {
let manifest_path = package.manifest_path.as_std_path();
let manifest_content = read_to_string(manifest_path)?;
let manifest: Manifest = toml::from_str(&manifest_content)?;
let resolved = metadata
.resolve
.as_ref()
.ok_or_else(|| {
anyhow!("`cargo_metadata::MetadataCommand::no_deps` should not be called.")
})?
.nodes
.iter()
.find(|node| node.id == package.id)
.ok_or_else(|| anyhow!("Package not found: {}", package.name))?;
let directory = manifest_path
.parent()
.ok_or_else(|| anyhow!("Package has no parent directory: {}", package.name))?
.to_path_buf();
let mut import_to_pkg = FxHashMap::default();
let mut pkg_to_import = FxHashMap::default();
for dep in &resolved.deps {
if let Some(pkg) = metadata.packages.iter().find(|package| package.id == dep.pkg) {
// Artifact/bindep dependencies have an empty `dep.name` in `cargo_metadata`.
// Fall back to the package name so the rest of the pipeline can match them.
let name =
if dep.name.is_empty() { pkg.name.replace('-', "_") } else { dep.name.clone() };
import_to_pkg.insert(name.clone(), pkg.name.to_string());
pkg_to_import.insert(pkg.name.to_string(), name);
}
}
let package_ignored_deps = &manifest.package.metadata.cargo_shear.ignored;
let workspace_ignored_deps = &workspace.manifest.workspace.metadata.cargo_shear.ignored;
let ignored_imports = package_ignored_deps
.iter()
.chain(workspace_ignored_deps)
.map(|dep| dep.get_ref().replace('-', "_"))
.collect();
Ok(Self {
workspace,
name: package.name.to_string(),
directory,
manifest_path: manifest_path.to_path_buf(),
manifest_content,
manifest,
targets: package.targets.clone(),
import_to_pkg,
pkg_to_import,
ignored_imports,
})
}
}