use crate::cache;
use crate::size;
use crate::types::{Config, CrateUnit, Edition, Error, Result, UnknownEdition};
use cargo_metadata::MetadataCommand;
use crossbeam_channel::Sender;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
mod warnings;
pub(crate) fn run(cfg: &Config, tx: &Sender<CrateUnit>) -> Result<Option<cache::Cache>> {
let mut cmd = MetadataCommand::new();
cmd.no_deps();
if let Some(p) = &cfg.manifest_path {
cmd.manifest_path(p);
}
let metadata = cmd.exec()?;
let ws_root = metadata.workspace_root.as_std_path().to_path_buf();
let ws_roots = WsRoots::new(&ws_root);
let mut cache_opt = cfg
.experimental_cache
.then(|| cache::Cache::load(metadata.workspace_root.as_std_path()));
let workspace_members: HashSet<&cargo_metadata::PackageId> =
metadata.workspace_members.iter().collect();
let at_root = at_workspace_root(cfg, metadata.workspace_root.as_std_path());
let selected = select_packages(cfg, &metadata, &workspace_members, at_root)?;
let mut claimed: HashMap<PathBuf, ClaimSite> = HashMap::new();
let mut editions_seen: HashMap<Edition, String> = HashMap::new();
let mut governed: HashMap<PathBuf, Vec<(String, Edition)>> = HashMap::new();
let mut implicit_2015: Vec<String> = Vec::new();
for pkg in &metadata.packages {
if !selected.contains(&pkg.id) {
continue;
}
let edition: Edition =
pkg.edition
.try_into()
.map_err(|UnknownEdition(year)| Error::UnsupportedEdition {
edition: year,
package: pkg.name.to_string(),
})?;
editions_seen
.entry(edition)
.or_insert_with(|| pkg.name.to_string());
let manifest_dir: PathBuf = pkg
.manifest_path
.parent()
.map(|p| p.as_std_path().to_path_buf())
.ok_or_else(|| {
Error::Io(std::io::Error::other(format!(
"manifest_path has no parent: {}",
pkg.manifest_path
)))
})?;
if cfg.warnings
&& let Some(cfg_file) = find_nearest_config(&manifest_dir, &ws_root)
{
governed
.entry(cfg_file)
.or_default()
.push((pkg.name.to_string(), edition));
}
if cfg.warnings
&& edition == Edition::E2015
&& !cargo_toml_declares_edition(pkg.manifest_path.as_std_path())
{
implicit_2015.push(pkg.name.to_string());
}
let entry_points =
collect_entry_points(pkg, edition, &mut claimed, cfg.warnings, &ws_roots);
if entry_points.is_empty() {
continue;
}
let size_bytes = if let Some(c) = cache_opt.as_mut() {
let built = cache::build(&manifest_dir);
if c.matches(&manifest_dir, &built.fingerprint) {
continue;
}
c.stage(manifest_dir.clone(), built.fingerprint);
built.size_bytes
} else {
size::estimate(&manifest_dir)
};
let unit = CrateUnit {
edition,
manifest_dir,
files: entry_points,
size_bytes,
};
if tx.send(unit).is_err() {
return Err(Error::SendClosed);
}
}
if cfg.warnings && editions_seen.len() > 1 {
warnings::emit_multi_edition_warning(&editions_seen);
}
if cfg.warnings {
warnings::emit_shadow_config_warning(&ws_roots, &governed);
warnings::emit_config_edition_warning(&ws_roots, &governed);
warnings::emit_implicit_edition_warning(&implicit_2015);
}
Ok(cache_opt)
}
fn collect_entry_points(
pkg: &cargo_metadata::Package,
edition: Edition,
claimed: &mut HashMap<PathBuf, ClaimSite>,
emit_warnings: bool,
roots: &WsRoots,
) -> Vec<PathBuf> {
let mut entry_points: Vec<PathBuf> = Vec::new();
for tgt in &pkg.targets {
let raw = tgt.src_path.as_std_path().to_path_buf();
let canon = raw.canonicalize().unwrap_or(raw);
let claim_site = ClaimSite {
name: pkg.name.to_string(),
edition,
manifest_path: pkg.manifest_path.as_std_path().to_path_buf(),
};
match claimed.entry(canon.clone()) {
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(claim_site);
entry_points.push(canon);
}
std::collections::hash_map::Entry::Occupied(o) => {
if emit_warnings {
warnings::emit_claim_collision_warning(roots, &canon, o.get(), &claim_site);
}
}
}
}
entry_points
}
fn select_packages<'a>(
cfg: &Config,
metadata: &'a cargo_metadata::Metadata,
workspace_members: &HashSet<&'a cargo_metadata::PackageId>,
at_root: bool,
) -> Result<HashSet<&'a cargo_metadata::PackageId>> {
Ok(if cfg.all {
workspace_members.clone()
} else if !cfg.packages.is_empty() {
let member_names: HashSet<&str> = metadata
.packages
.iter()
.filter(|p| workspace_members.contains(&p.id))
.map(|p| p.name.as_str())
.collect();
let unknown: Vec<String> = cfg
.packages
.iter()
.filter(|n| !member_names.contains(n.as_str()))
.cloned()
.collect();
if !unknown.is_empty() {
return Err(Error::UnknownPackages(unknown));
}
let names: HashSet<&str> = cfg.packages.iter().map(String::as_str).collect();
metadata
.packages
.iter()
.filter(|p| workspace_members.contains(&p.id) && names.contains(p.name.as_str()))
.map(|p| &p.id)
.collect()
} else if at_root {
workspace_members.clone()
} else if let Some(root) = metadata.root_package() {
std::iter::once(&root.id).collect()
} else {
metadata
.workspace_default_packages()
.into_iter()
.map(|p| &p.id)
.collect()
})
}
fn at_workspace_root(cfg: &Config, ws_root: &Path) -> bool {
let Ok(ws_manifest) = ws_root.join("Cargo.toml").canonicalize() else {
return false;
};
let effective = match &cfg.manifest_path {
Some(p) => p.canonicalize().ok(),
None => std::env::current_dir()
.ok()
.and_then(|cwd| find_manifest_upward(&cwd)),
};
effective.is_some_and(|m| m == ws_manifest)
}
fn find_manifest_upward(start: &Path) -> Option<PathBuf> {
let mut p = start.canonicalize().ok()?;
loop {
let cand = p.join("Cargo.toml");
if cand.is_file() {
return cand.canonicalize().ok();
}
if !p.pop() {
return None;
}
}
}
fn find_nearest_config(start: &Path, root: &Path) -> Option<PathBuf> {
let mut dir = start;
loop {
for name in [".rustfmt.toml", "rustfmt.toml"] {
let cand = dir.join(name);
if cand.is_file() {
return Some(cand);
}
}
if dir == root {
return None;
}
dir = dir.parent()?;
}
}
fn cargo_toml_declares_edition(manifest_path: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(manifest_path) else {
return true;
};
content.lines().any(|line| {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("edition") else {
return false;
};
let rest = rest.trim_start();
rest.starts_with('=') || rest.starts_with('.')
})
}
struct ClaimSite {
name: String,
edition: Edition,
manifest_path: PathBuf,
}
pub(super) struct WsRoots {
raw: PathBuf,
canon: PathBuf,
}
impl WsRoots {
fn new(raw: &Path) -> Self {
Self {
raw: raw.to_path_buf(),
canon: raw.canonicalize().unwrap_or_else(|_| raw.to_path_buf()),
}
}
pub(super) fn raw(&self) -> &Path {
&self.raw
}
pub(super) fn rel<'p>(&self, p: &'p Path) -> &'p Path {
p.strip_prefix(&self.raw)
.or_else(|_| p.strip_prefix(&self.canon))
.unwrap_or(p)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::{WsRoots, cargo_toml_declares_edition, find_nearest_config};
use std::path::PathBuf;
fn unique_tmp(prefix: &str) -> PathBuf {
std::env::temp_dir().join(format!("{prefix}-{}", std::process::id()))
}
#[test]
fn cargo_toml_declares_edition_detects_explicit_and_inherited_keys() {
let dir = unique_tmp("ff-disc-edition");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let explicit = dir.join("explicit.toml");
std::fs::write(&explicit, "[package]\nname = \"x\"\nedition = \"2021\"\n").unwrap();
assert!(cargo_toml_declares_edition(&explicit));
let inherited = dir.join("inherited.toml");
std::fs::write(&inherited, "[package]\nedition.workspace = true\n").unwrap();
assert!(cargo_toml_declares_edition(&inherited));
let absent = dir.join("absent.toml");
std::fs::write(&absent, "[package]\nname = \"x\"\n").unwrap();
assert!(!cargo_toml_declares_edition(&absent));
assert!(cargo_toml_declares_edition(&dir.join("missing.toml")));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn find_nearest_config_walks_up_and_prefers_the_closest() {
let root = unique_tmp("ff-disc-config");
let _ = std::fs::remove_dir_all(&root);
let nested = root.join("crates").join("a");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(root.join("rustfmt.toml"), "edition = \"2021\"\n").unwrap();
assert_eq!(
find_nearest_config(&nested, &root),
Some(root.join("rustfmt.toml"))
);
std::fs::write(nested.join(".rustfmt.toml"), "").unwrap();
assert_eq!(
find_nearest_config(&nested, &root),
Some(nested.join(".rustfmt.toml"))
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn ws_roots_rel_strips_the_workspace_prefix() {
let roots = WsRoots::new(&PathBuf::from("/ws"));
assert_eq!(
roots.rel(&PathBuf::from("/ws/crates/a/Cargo.toml")),
PathBuf::from("crates/a/Cargo.toml")
);
assert_eq!(
roots.rel(&PathBuf::from("/elsewhere/x")),
PathBuf::from("/elsewhere/x")
);
}
}