Skip to main content

cargo_ff/
types.rs

1use std::path::PathBuf;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum Edition {
5    E2015,
6    E2018,
7    E2021,
8    E2024,
9}
10
11impl Edition {
12    pub fn as_str(self) -> &'static str {
13        match self {
14            Edition::E2015 => "2015",
15            Edition::E2018 => "2018",
16            Edition::E2021 => "2021",
17            Edition::E2024 => "2024",
18        }
19    }
20}
21
22impl std::str::FromStr for Edition {
23    type Err = UnknownEdition;
24    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
25        Ok(match s {
26            "2015" => Edition::E2015,
27            "2018" => Edition::E2018,
28            "2021" => Edition::E2021,
29            "2024" => Edition::E2024,
30            other => return Err(UnknownEdition(other.to_owned())),
31        })
32    }
33}
34
35impl TryFrom<cargo_metadata::Edition> for Edition {
36    type Error = UnknownEdition;
37    fn try_from(e: cargo_metadata::Edition) -> std::result::Result<Self, Self::Error> {
38        e.as_str().parse()
39    }
40}
41
42/// Edition string we don't recognize. Future variants (2027, 2030, …) land
43/// here. Caller wraps with package context before surfacing.
44#[derive(Debug, Clone, thiserror::Error)]
45#[error("unknown edition: {0}")]
46pub struct UnknownEdition(pub String);
47
48#[derive(Debug, Clone)]
49pub struct CrateUnit {
50    pub edition: Edition,
51    pub manifest_dir: PathBuf,
52    pub files: Vec<PathBuf>,
53    /// Work-size proxy: sum of `*.rs` bytes under `manifest_dir`,
54    /// clamped at `size::HUGE_CUTOFF_BYTES`. Drives LPT bin-packing in
55    /// the coalescer and dispatch ordering in the priority queue. Only
56    /// the *ratios* between crates matter; a value exactly equal to the
57    /// cutoff means the crate is at-or-above the solo-dispatch threshold.
58    pub size_bytes: u64,
59}
60
61/// One rustfmt invocation's worth of work: a homogeneous-edition group of
62/// crates whose entry-point files are passed together to a single rustfmt
63/// process. With batch_size=1 this is equivalent to per-crate dispatch.
64#[derive(Debug, Clone)]
65pub struct Batch {
66    pub edition: Edition,
67    pub units: Vec<CrateUnit>,
68}
69
70impl Batch {
71    pub fn size_bytes(&self) -> u64 {
72        self.units.iter().map(|u| u.size_bytes).sum()
73    }
74
75    pub fn file_count(&self) -> usize {
76        self.units.iter().map(|u| u.files.len()).sum()
77    }
78
79    /// Sort key for deterministic output ordering: lex-min of manifest dirs.
80    pub fn sort_key(&self) -> PathBuf {
81        self.units
82            .iter()
83            .map(|u| u.manifest_dir.clone())
84            .min()
85            .unwrap_or_default()
86    }
87}
88
89#[derive(Debug)]
90pub struct BatchResult {
91    pub sort_key: PathBuf,
92    pub stdout: Vec<u8>,
93    pub stderr: Vec<u8>,
94    pub exit_code: i32,
95    /// All files in the batch, with the manifest_dir they came from.
96    /// Used by the aggregator to attribute `--check` failures back to
97    /// individual crates.
98    pub files: Vec<(PathBuf, PathBuf)>,
99}
100
101#[derive(Debug, Clone, Default)]
102pub struct Config {
103    pub manifest_path: Option<PathBuf>,
104    pub packages: Vec<String>,
105    /// Format every workspace member, regardless of `manifest_path`'s implicit
106    /// package selection. Mirrors `cargo fmt --all`.
107    pub all: bool,
108    pub check: bool,
109    pub workers: Option<usize>,
110    pub channel_capacity: Option<usize>,
111    pub rustfmt_args: Vec<String>,
112    /// Crates per rustfmt invocation. Higher values amortize spawn cost
113    /// (~40ms/invocation on M-series) at the cost of coarser scheduling
114    /// granularity. `None` → 3 (the curve is flat across bs=12-48 thanks
115    /// to the priority queue, so a small default keeps tiny workspaces
116    /// from collapsing into one batch).
117    pub batch_size: Option<usize>,
118    /// Experimental: skip rustfmt for crates whose `*.rs` file mtimes
119    /// match the prior successful run. After a write that actually
120    /// rewrote files, the next run will miss (mtimes shifted) and
121    /// re-dispatch — correct, just one wasted invocation. See
122    /// `cache.rs` for soundness caveats.
123    pub experimental_cache: bool,
124    /// Emit advisory warnings to stderr (cross-crate file claims,
125    /// stable-rustfmt-on-PATH). Off by default — these are silent in
126    /// stock `cargo fmt` too, and the noise isn't worth it for most users.
127    pub warnings: bool,
128}
129
130#[derive(Debug)]
131pub struct Report {
132    pub failures: Vec<FileFailure>,
133    pub exit_code: i32,
134}
135
136#[derive(Debug)]
137pub struct FileFailure {
138    pub file: PathBuf,
139    pub manifest_dir: PathBuf,
140}
141
142#[derive(Debug, thiserror::Error)]
143pub enum Error {
144    #[error("`workers` must be >= 1 (got {0})")]
145    InvalidWorkers(usize),
146    #[error("cargo metadata failed: {0}")]
147    Metadata(#[from] cargo_metadata::Error),
148    #[error("io: {0}")]
149    Io(#[from] std::io::Error),
150    #[error(
151        "unsupported edition `{edition}` for package `{package}`; cargo-ff knows 2015/2018/2021/2024 — bump the dep or pin a known edition"
152    )]
153    UnsupportedEdition { edition: String, package: String },
154    #[error("package(s) not found in the workspace: {}", .0.join(", "))]
155    UnknownPackages(Vec<String>),
156    #[error("{0} thread panicked")]
157    ThreadPanicked(&'static str),
158    #[error("send failed (channel closed)")]
159    SendClosed,
160}
161
162pub type Result<T> = std::result::Result<T, Error>;