use std::path::Path;
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use cairo_lang_formatter::{CairoFormatter, FormatOutcome, FormatterConfig, StdinFmt};
use cairo_lang_utils::logging::init_logging;
use clap::Parser;
use colored::Colorize;
use ignore::{DirEntry, Error, ParallelVisitor, ParallelVisitorBuilder, WalkState};
use log::warn;
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn eprintln_if_verbose(s: &str, verbose: bool) {
if verbose {
eprintln!("{s}");
}
}
#[derive(Parser, Debug)]
#[command(version, verbatim_doc_comment)]
struct FormatterArgs {
#[arg(short, long, default_value_t = false)]
check: bool,
#[arg(short, long, default_value_t = false)]
recursive: bool,
#[arg(short, long, default_value_t = false)]
verbose: bool,
#[arg(short, long, default_value_t = false)]
print_parsing_errors: bool,
#[arg(short, long)]
sort_mod_level_items: Option<bool>,
#[arg(long)]
tuple_line_breaking: Option<bool>,
#[arg(long)]
fixed_array_line_breaking: Option<bool>,
#[arg(long)]
macro_call_breaking_behavior: Option<bool>,
#[arg(long)]
merge_use_items: Option<bool>,
#[arg(long)]
allow_duplicates: Option<bool>,
files: Vec<String>,
}
fn print_error(error: String, path: String, args: &FormatterArgs) {
let parsed_errors = if args.print_parsing_errors {
error.red()
} else {
"Run with '--print-parsing-errors' to see error details.".red()
};
eprintln!(
"{}",
format!(
"A parsing error occurred in {path}. The content was not formatted.\n{parsed_errors}"
)
.red()
);
}
struct PathFormatter<'t> {
all_correct: &'t AtomicBool,
args: &'t FormatterArgs,
fmt: &'t CairoFormatter,
}
struct PathFormatterBuilder<'t> {
all_correct: &'t AtomicBool,
args: &'t FormatterArgs,
fmt: &'t CairoFormatter,
}
impl<'s, 't> ParallelVisitorBuilder<'s> for PathFormatterBuilder<'t>
where
't: 's,
{
fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
Box::new(PathFormatter { all_correct: self.all_correct, args: self.args, fmt: self.fmt })
}
}
fn check_file_formatting(fmt: &CairoFormatter, args: &FormatterArgs, path: &Path) -> bool {
match fmt.format_to_string(&path) {
Ok(FormatOutcome::Identical(_)) => true,
Ok(FormatOutcome::DiffFound(diff)) => {
println!("Diff found in file {}:\n {}", path.display(), diff.display_colored());
false
}
Err(parsing_error) => {
print_error(parsing_error.to_string(), path.display().to_string(), args);
false
}
}
}
fn format_file_in_place(fmt: &CairoFormatter, args: &FormatterArgs, path: &Path) -> bool {
if let Err(parsing_error) = fmt.format_in_place(&path) {
print_error(parsing_error.to_string(), path.display().to_string(), args);
false
} else {
true
}
}
impl ParallelVisitor for PathFormatter<'_> {
fn visit(&mut self, dir_entry_res: Result<DirEntry, Error>) -> WalkState {
let dir_entry = if let Ok(dir_entry) = dir_entry_res {
dir_entry
} else {
warn!("Failed to read the file.");
return WalkState::Continue;
};
let file_type = if let Some(file_type) = dir_entry.file_type() {
file_type
} else {
warn!("Failed to read filetype.");
return WalkState::Continue;
};
if !file_type.is_file() {
return WalkState::Continue;
}
let file_path = dir_entry.path();
if self.args.verbose {
eprintln!("Formatting file: {}.", file_path.display());
}
let success = if self.args.check {
check_file_formatting(self.fmt, self.args, file_path)
} else {
format_file_in_place(self.fmt, self.args, file_path)
};
if !success {
self.all_correct.store(false, Ordering::Release);
}
WalkState::Continue
}
}
fn format_path(start_path: &str, args: &FormatterArgs, fmt: &CairoFormatter) -> bool {
let base = Path::new(start_path);
let mut walk = fmt.walk(base);
if !args.recursive {
walk.max_depth(Some(1));
}
let all_correct = AtomicBool::new(true);
let mut builder = PathFormatterBuilder { args, fmt, all_correct: &all_correct };
walk.build_parallel().visit(&mut builder);
builder.all_correct.load(Ordering::Acquire)
}
fn format_stdin(args: &FormatterArgs, fmt: &CairoFormatter) -> bool {
match fmt.format_to_string(&StdinFmt) {
Ok(outcome) => {
if args.check {
match outcome {
FormatOutcome::Identical(_) => true,
FormatOutcome::DiffFound(diff) => {
println!("{diff}");
false
}
}
} else {
print!("{}", FormatOutcome::into_output_text(outcome));
true
}
}
Err(parsing_error) => {
print_error(parsing_error.to_string(), String::from("standard input"), args);
false
}
}
}
fn main() -> ExitCode {
init_logging(tracing::Level::ERROR);
log::info!("Starting formatting.");
let args = FormatterArgs::parse();
let config = FormatterConfig::default()
.sort_module_level_items(args.sort_mod_level_items)
.tuple_breaking_behavior(args.tuple_line_breaking.map(Into::into))
.fixed_array_breaking_behavior(args.fixed_array_line_breaking.map(Into::into))
.macro_call_breaking_behavior(args.macro_call_breaking_behavior.map(Into::into))
.merge_use_items(args.merge_use_items)
.allow_duplicate_uses(args.allow_duplicates);
let fmt = CairoFormatter::new(config);
eprintln_if_verbose(
&format!("Start formatting. Check: {}, Recursive: {}.", args.check, args.recursive),
args.verbose,
);
let all_correct = if args.files.len() == 1 && args.files[0] == "-" {
format_stdin(&args, &fmt)
} else if args.files.is_empty() {
format_path(".", &args, &fmt)
} else {
args.files.iter().all(|file| format_path(file, &args, &fmt))
};
if all_correct { ExitCode::SUCCESS } else { ExitCode::FAILURE }
}