#![doc = include_str!("../README.md")]
extern crate alloc;
mod ast;
mod context;
mod error;
mod file;
mod format;
mod parse;
mod position;
mod position_map;
use self::file::read_paths;
use crate::{
file::display_path,
format::format,
parse::{ParseError, parse, parse_comments, parse_hash_directives},
position_map::PositionMap,
};
use bumpalo::Bump;
use clap::Parser;
use colored::Colorize;
use core::error::Error;
use error::ApplicationError;
use futures::future::try_join_all;
use std::{env::current_dir, path::Path, process::ExitCode};
use tokio::{
fs::{read_to_string, write},
io::{AsyncReadExt, AsyncWriteExt, stdin, stdout},
spawn,
};
#[derive(clap::Parser)]
#[command(about, version)]
struct Arguments {
#[arg()]
path: Vec<String>,
#[arg(short, long)]
check: bool,
#[arg(short, long)]
ignore: Vec<String>,
#[arg(short, long)]
verbose: bool,
}
#[tokio::main]
async fn main() -> ExitCode {
if let Err(error) = run(Arguments::parse()).await {
eprintln!("{error}");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
async fn run(
Arguments {
path,
check,
ignore,
verbose,
..
}: Arguments,
) -> Result<(), Box<dyn Error>> {
if path.is_empty() && check {
Err("cannot check stdin".into())
} else if path.is_empty() {
format_stdin().await
} else if check {
check_paths(&path, &ignore, verbose).await
} else {
format_paths(&path, &ignore, verbose).await
}
}
async fn check_paths(
paths: &[String],
ignore_patterns: &[String],
verbose: bool,
) -> Result<(), Box<dyn Error>> {
let directory = current_dir()?;
let mut count = 0;
let mut error_count = 0;
for (result, path) in try_join_all(
read_paths(&directory, paths, ignore_patterns)?
.map(|path| spawn(async { (check_path(&path).await, path) })),
)
.await?
{
count += 1;
match result {
Ok(success) => {
if !success {
eprintln!("{}\t{}", "FAIL".yellow(), display_path(&path, &directory));
error_count += 1;
} else if verbose {
eprintln!("{}\t{}", "OK".green(), display_path(&path, &directory));
}
}
Err(error) => {
eprintln!(
"{}\t{}\t{}",
"ERROR".red(),
display_path(&path, &directory),
error
);
error_count += 1;
}
}
}
if error_count == 0 {
Ok(())
} else {
Err(format!("{error_count} / {count} file(s) failed").into())
}
}
async fn format_paths(
paths: &[String],
ignore_patterns: &[String],
verbose: bool,
) -> Result<(), Box<dyn Error>> {
let directory = current_dir()?;
let mut count = 0;
let mut error_count = 0;
for (result, path) in try_join_all(
read_paths(&directory, paths, ignore_patterns)?
.map(|path| spawn(async { (format_path(&path).await, path) })),
)
.await?
{
count += 1;
match result {
Ok(_) => {
if verbose {
eprintln!("{}\t{}", "FORMAT".blue(), display_path(&path, &directory));
}
}
Err(error) => {
eprintln!(
"{}\t{}\t{}",
"ERROR".red(),
display_path(&path, &directory),
error
);
error_count += 1;
}
}
}
if error_count == 0 {
Ok(())
} else {
Err(format!("{error_count} / {count} file(s) failed to format").into())
}
}
async fn format_stdin() -> Result<(), Box<dyn Error>> {
let mut source = Default::default();
stdin().read_to_string(&mut source).await?;
let position_map = PositionMap::new(&source);
let convert_error = |error| convert_parse_error(error, &source, &position_map);
let allocator = Bump::new();
let mut stdout = stdout();
stdout
.write_all(
format(
&parse(&source, &allocator).map_err(convert_error)?,
&parse_comments(&source, &allocator).map_err(convert_error)?,
&parse_hash_directives(&source, &allocator).map_err(convert_error)?,
&position_map,
&allocator,
)?
.as_bytes(),
)
.await?;
stdout.flush().await?;
Ok(())
}
async fn check_path(path: &Path) -> Result<bool, ApplicationError> {
let source = read_to_string(path).await?;
Ok(source == format_string(&source)?)
}
async fn format_path(path: &Path) -> Result<(), ApplicationError> {
let source = read_to_string(path).await?;
let formatted = format_string(&source)?;
if source != formatted {
write(path, formatted).await?;
}
Ok(())
}
fn format_string(source: &str) -> Result<String, ApplicationError> {
let position_map = PositionMap::new(source);
let convert_error = |error: ParseError| convert_parse_error(error, source, &position_map);
let allocator = Bump::new();
let source = format(
&parse(source, &allocator).map_err(convert_error)?,
&parse_comments(source, &allocator).map_err(convert_error)?,
&parse_hash_directives(source, &allocator).map_err(convert_error)?,
&position_map,
&allocator,
)?;
Ok(source)
}
fn convert_parse_error(
error: ParseError,
source: &str,
position_map: &PositionMap,
) -> ApplicationError {
ApplicationError::Parse(error.to_string(source, position_map))
}