Skip to main content

cargo_ff/
types.rs

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