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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
//! 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};
/// Marker that `cargo hakari` writes into a `workspace-hack` crate's `Cargo.toml`
/// to delimit its generated dependency section. Its presence uniquely identifies
/// such a crate, regardless of the name the user gave it. See
/// <https://docs.rs/cargo-hakari>.
const HAKARI_SECTION_MARKER: &str = "### BEGIN HAKARI SECTION";
/// 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>,
/// Names of workspace members that are `cargo hakari` `workspace-hack` crates,
/// detected by the `### BEGIN HAKARI SECTION` marker in their `Cargo.toml`.
/// They exist solely to unify Cargo features: they declare many dependencies
/// they never import, and every member depends on them without importing them,
/// so they're exempt from unused-dependency analysis entirely.
pub hakari_packages: 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();
let hakari_packages = Self::detect_hakari_packages(metadata);
Ok(Self {
root,
manifest_path,
manifest_content,
manifest,
files,
linked,
packages,
dep_to_pkg,
ignored_deps,
hakari_packages,
})
}
/// Identify the workspace members that are `cargo hakari` `workspace-hack`
/// crates by scanning each member's `Cargo.toml` for the hakari section
/// marker, returning their package names. The manifests are read in parallel,
/// like every other bulk pass in [`Self::new`].
fn detect_hakari_packages(metadata: &Metadata) -> FxHashSet<String> {
metadata
.workspace_packages()
.into_par_iter()
.filter(|pkg| {
read_to_string(pkg.manifest_path.as_std_path())
.is_ok_and(|content| content.contains(HAKARI_SECTION_MARKER))
})
.map(|pkg| pkg.name.to_string())
.collect()
}
}
/// 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,
})
}
}