use super::GlobalArgs;
use crate::{autofix::*, cmd::parse_key_val, grammar::*, log};
use cargo_metadata::Metadata;
use std::{collections::BTreeMap as Map, fs::canonicalize, path::PathBuf, str::FromStr};
#[derive(Debug, clap::Parser)]
pub struct FormatCmd {
#[clap(subcommand)]
subcommand: SubCommand,
}
#[derive(Debug, clap::Subcommand)]
pub enum SubCommand {
#[clap(alias = "f")]
Features(FormatFeaturesCmd),
}
#[derive(Debug, clap::Parser)]
pub struct FormatFeaturesCmd {
#[allow(missing_docs)]
#[clap(flatten)]
cargo_args: super::CargoArgs,
#[clap(long)]
no_workspace: bool,
#[clap(long)]
modify_paths: Vec<PathBuf>,
#[clap(long = "check", short = 'c')]
unused_check: bool,
#[clap(long, short)]
fix: bool,
#[clap(long, default_value_t = 80)]
line_width: u32,
#[clap(long, value_name = "FEATURE:MODE", value_parser = parse_key_val::<String, Mode>, value_delimiter = ',', verbatim_doc_comment)]
mode_per_feature: Option<Vec<(String, Mode)>>,
#[clap(long, value_name = "FEATURE", value_delimiter = ',', verbatim_doc_comment)]
ignore_feature: Vec<String>,
#[clap(long)]
print_paths: bool,
}
#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
pub enum Mode {
None,
Sort,
Dedub,
Canonicalize,
}
impl FromStr for Mode {
type Err = std::string::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"canonicalize" => Ok(Self::Canonicalize),
"sort" => Ok(Self::Sort),
"none" => Ok(Self::None),
_ => panic!("Invalid Mode: {s}. Expected 'canonicalize', 'sort' or 'none'"), }
}
}
impl FormatCmd {
pub fn run(&self, global: &GlobalArgs) {
match &self.subcommand {
SubCommand::Features(cmd) => cmd.run(global),
}
}
}
impl FormatFeaturesCmd {
pub fn run(&self, global: &GlobalArgs) {
if self.unused_check {
log::warn!("The `--check` is now implicit and ignored");
}
let modes = self.parse_mode_per_feature();
let meta = self.load_metadata(global);
let allowed_dir = canonicalize(meta.workspace_root.as_std_path()).unwrap();
log::debug!("Allowed dir: {}", allowed_dir.display());
let mut offenders = Vec::new();
let mut errors = Map::<(PathBuf, String), Vec<String>>::new();
log::debug!("Checking {} crate{}", meta.packages.len(), plural(meta.packages.len()));
for pkg in meta.packages.iter() {
let path = canonicalize(pkg.manifest_path.clone().into_std_path_buf()).unwrap();
let mut fixer = AutoFixer::from_manifest(&path).unwrap();
if let Err(errs) = fixer.canonicalize_features(&pkg.name, &modes, self.line_width) {
let path = path.strip_prefix(&allowed_dir).unwrap().to_path_buf();
errors.entry((path.clone(), pkg.name.to_string())).or_default().extend(errs);
} else if fixer.modified() {
offenders.push((path, &pkg.name, fixer));
}
}
if !errors.is_empty() {
let num_errors = errors.values().map(|errs| errs.len()).sum::<usize>();
println!(
"Please fix {} error{} in {} crate{} manually:",
global.red(&num_errors.to_string()),
plural(num_errors),
global.red(&errors.len().to_string()),
plural(errors.len())
);
for ((path, pkg), errs) in errors.iter() {
println!(" {} ({})", global.bold(pkg), path.display());
for err in errs.iter() {
println!(" {err}");
}
}
std::process::exit(global.error_code())
}
if offenders.is_empty() {
log::debug!(
"Checked {} crate{}: all formatted",
meta.packages.len(),
plural(meta.packages.len())
);
return
}
let mut fixed = 0;
println!(
"Found {} crate{} with unformatted features:",
global.red(&offenders.len().to_string()),
plural(offenders.len())
);
for (path, pkg, fixer) in offenders.iter_mut() {
let psuffix =
if self.print_paths { format!(" {}", path.display()) } else { Default::default() };
println!(" {}{}", global.bold(pkg), psuffix);
if !self.fix {
continue
}
let can_modify = path.starts_with(&allowed_dir) ||
self.modify_paths.iter().any(|p| path.starts_with(p));
if !can_modify {
log::warn!(
"Not allowed to modify {} outside of: {}",
path.display(),
allowed_dir.display()
);
continue
}
fixer.save().unwrap();
fixed += 1;
}
if self.fix {
if fixed == offenders.len() {
println!(
"Formatted {} crate{} (all fixed).",
global.green(&fixed.to_string()),
plural(fixed)
);
} else {
println!(
"Formatted {} crate{} ({} could not be fixed).",
global.green(&fixed.to_string()),
plural(fixed),
global.red(&(offenders.len() - fixed).to_string())
);
}
std::process::exit(0);
} else if global.show_hints() {
println!("Run again with `--fix` to format them.");
}
std::process::exit(global.error_code())
}
fn parse_mode_per_feature(&self) -> Map<String, Vec<Mode>> {
let mut map = Map::<String, Vec<Mode>>::new();
if let Some(modes) = &self.mode_per_feature {
for (feature, mode) in modes.iter() {
map.entry(feature.clone()).or_default().push(mode.clone());
}
}
for feature in self.ignore_feature.iter() {
map.insert(feature.clone(), vec![Mode::None]);
}
map
}
fn load_metadata(&self, global: &GlobalArgs) -> Metadata {
let mut args = self.cargo_args.clone();
if args.workspace {
println!("{}", global.yellow("WARNING: --workspace is the default now"));
}
args.workspace = !self.no_workspace;
match args.load_metadata() {
Ok(meta) => meta,
Err(err) => {
println!("{}", global.red(&err));
std::process::exit(1)
},
}
}
}