use crate::adapter::rust::manifest::CargoToml;
use crate::adapter::{RustAdapter, RustBuildOptions};
use crate::cli::Ui;
use clap::Args;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Args, Debug, Clone, Default)]
pub struct FmtArgs {
#[arg(long)]
pub manifest_path: Option<PathBuf>,
#[arg(short, long)]
pub package: Option<String>,
#[arg(long)]
pub check: bool,
#[arg(long)]
pub all: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(last = true)]
pub extra_args: Vec<String>,
}
fn resolve_rustfmt() -> PathBuf {
if let Ok(p) = std::env::var("RUSTFMT") {
return PathBuf::from(p);
}
if let Ok(home) = std::env::var("HOME") {
let p = PathBuf::from(home)
.join(".cargo")
.join("bin")
.join("rustfmt");
if p.is_file() {
return p;
}
}
PathBuf::from("rustfmt")
}
fn collect_named_dir_roots(
pkg_dir: &Path,
subdir: &str,
explicit: &[crate::adapter::rust::manifest_models::TomlTest],
auto_enabled: bool,
files: &mut Vec<PathBuf>,
) {
let mut explicit_paths = std::collections::HashSet::new();
for item in explicit {
let p = match item.path {
Some(ref rel) => pkg_dir.join(rel),
None => pkg_dir.join(subdir).join(format!("{}.rs", item.name)),
};
if p.is_file() {
explicit_paths.insert(p.clone());
files.push(p);
}
}
if !auto_enabled {
return;
}
let dir = pkg_dir.join(subdir);
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file()
&& p.extension().and_then(|e| e.to_str()) == Some("rs")
&& !explicit_paths.contains(&p)
{
if p.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|s| !s.starts_with('.'))
{
files.push(p);
}
} else if p.is_dir() {
let main_rs = p.join("main.rs");
if main_rs.is_file()
&& !explicit_paths.contains(&main_rs)
&& p.file_name()
.and_then(|s| s.to_str())
.is_some_and(|s| !s.starts_with('.'))
{
files.push(main_rs);
}
}
}
}
}
fn collect_package_source_files(pkg_dir: &Path, toml: &CargoToml, files: &mut Vec<PathBuf>) {
for t in toml.discover_targets(pkg_dir) {
if t.src_path.is_file() {
files.push(t.src_path);
}
}
for t in toml.discover_test_targets(pkg_dir) {
if t.src_path.is_file() {
files.push(t.src_path);
}
}
let autoexamples = toml
.package
.as_ref()
.and_then(|p| p.autoexamples)
.unwrap_or(true);
collect_named_dir_roots(pkg_dir, "examples", &toml.example, autoexamples, files);
let autobenches = toml
.package
.as_ref()
.and_then(|p| p.autobenches)
.unwrap_or(true);
collect_named_dir_roots(pkg_dir, "benches", &toml.bench, autobenches, files);
if toml.has_build_script(pkg_dir) {
let build_path = match toml.package.as_ref().and_then(|p| p.build.as_ref()) {
Some(crate::adapter::rust::manifest_models::TomlBuild::Path(ref p)) => pkg_dir.join(p),
_ => pkg_dir.join("build.rs"),
};
if build_path.is_file() {
files.push(build_path);
}
}
}
pub fn handle_fmt(args: &FmtArgs) -> i32 {
let start = std::time::Instant::now();
let opt = RustBuildOptions {
manifest_path: args.manifest_path.clone(),
package: args.package.clone(),
verbose: args.verbose,
..Default::default()
};
let manifest_path = match RustAdapter::resolve_manifest(&opt) {
Ok(p) => p,
Err(err) => {
Ui::report_error(&err);
return 1;
}
};
let project_dir = manifest_path.parent().unwrap_or(Path::new("."));
let root_toml = match CargoToml::from_file(&manifest_path) {
Ok(t) => t,
Err(err) => {
Ui::report_error(&err);
return 1;
}
};
let mut members = match root_toml.discover_workspace_members(project_dir) {
Ok(m) => m,
Err(err) => {
Ui::report_error(&err);
return 1;
}
};
if args.all {
let mut queue = std::collections::VecDeque::new();
let mut visited_dirs = std::collections::HashSet::new();
for (_, m_dir) in &members {
visited_dirs.insert(m_dir.canonicalize().unwrap_or_else(|_| m_dir.clone()));
queue.push_back(m_dir.clone());
}
let enqueue_deps =
|base: &Path,
deps: &HashMap<String, crate::adapter::rust::manifest::TomlDependency>,
queue: &mut std::collections::VecDeque<PathBuf>,
visited: &mut std::collections::HashSet<PathBuf>| {
for dep in deps.values() {
if let crate::adapter::rust::manifest::TomlDependency::Detailed(ref d) = dep {
if let Some(ref rel) = d.path {
let dep_dir = base.join(rel);
if dep_dir.join("Cargo.toml").is_file() {
let canon =
dep_dir.canonicalize().unwrap_or_else(|_| dep_dir.clone());
if visited.insert(canon) {
queue.push_back(dep_dir);
}
}
}
}
}
};
if let Some(ref ws) = root_toml.workspace {
enqueue_deps(project_dir, &ws.dependencies, &mut queue, &mut visited_dirs);
}
while let Some(dir) = queue.pop_front() {
let m_path = dir.join("Cargo.toml");
let t = match CargoToml::from_file(&m_path) {
Ok(t) => t,
Err(err) => {
Ui::report_error(&err);
return 1;
}
};
enqueue_deps(&dir, &t.dependencies, &mut queue, &mut visited_dirs);
enqueue_deps(&dir, &t.dev_dependencies, &mut queue, &mut visited_dirs);
enqueue_deps(&dir, &t.build_dependencies, &mut queue, &mut visited_dirs);
for target_cfg in t.target.values() {
enqueue_deps(
&dir,
&target_cfg.dependencies,
&mut queue,
&mut visited_dirs,
);
enqueue_deps(
&dir,
&target_cfg.dev_dependencies,
&mut queue,
&mut visited_dirs,
);
enqueue_deps(
&dir,
&target_cfg.build_dependencies,
&mut queue,
&mut visited_dirs,
);
}
if let Some(pkg) = t.package {
if !members.iter().any(|(n, _)| n == &pkg.name) {
members.push((pkg.name, dir));
}
}
}
}
let mut edition_groups: HashMap<String, Vec<PathBuf>> = HashMap::new();
for (name, member_dir) in &members {
if let Some(ref target_pkg) = args.package {
if name != target_pkg {
continue;
}
}
let m_path = member_dir.join("Cargo.toml");
let toml = match CargoToml::from_file(&m_path) {
Ok(t) => t,
Err(err) => {
Ui::report_error(&err);
return 1;
}
};
let pkg_edition = toml
.package
.as_ref()
.and_then(|p| p.edition.clone())
.unwrap_or_else(|| "2015".to_string());
let mut pkg_files = Vec::new();
collect_package_source_files(member_dir, &toml, &mut pkg_files);
edition_groups
.entry(pkg_edition)
.or_default()
.extend(pkg_files);
}
let mut total_files = 0;
for files in edition_groups.values_mut() {
files.sort();
files.dedup();
total_files += files.len();
}
if total_files == 0 {
println!("mitts fmt: no source files found to format");
return 0;
}
let rustfmt_bin = resolve_rustfmt();
let mode_str = if args.check { "check" } else { "format" };
Ui::header(&format!("fmt [{}]", mode_str));
let mut editions_desc: Vec<String> = edition_groups
.iter()
.map(|(ed, fls)| format!("{} files (edition {})", fls.len(), ed))
.collect();
editions_desc.sort();
Ui::stage_start(
"🎨",
&format!("Running rustfmt over {}", editions_desc.join(", ")),
);
let mut sorted_editions: Vec<_> = edition_groups.iter().collect();
sorted_editions.sort_by_key(|(ed, _)| *ed);
for (edition, files) in sorted_editions {
for chunk in files.chunks(256) {
let mut cmd = Command::new(&rustfmt_bin);
cmd.arg("--edition").arg(edition);
if args.check {
cmd.arg("--check");
}
if args.verbose > 0 {
cmd.arg("-v");
}
cmd.args(&args.extra_args);
cmd.args(chunk);
let status = match cmd.status() {
Ok(s) => s,
Err(err) => {
eprintln!("mitts error: failed to invoke rustfmt: {}", err);
return 1;
}
};
if !status.success() {
let dur = format!("{:.2}s", start.elapsed().as_secs_f64());
Ui::finish_failed(
"fmt",
&dur,
"Formatting differences detected or rustfmt failed",
);
return status.code().unwrap_or(1);
}
}
}
let dur = format!("{:.2}s", start.elapsed().as_secs_f64());
let summary = if args.check {
"Formatting check passed (0 diffs)"
} else {
"Formatted files successfully"
};
Ui::stage_item(true, &Ui::badge_cached(), summary);
Ui::finish("fmt", &dur, summary);
0
}