use anyhow::{bail, Context, Result};
use console::style;
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use std::fs;
use std::io::{stdin, stdout, Read, Write};
use std::path::Path;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use std::time::Instant;
use structopt::StructOpt;
use threadpool::ThreadPool;
use stylua_lib::{format_code, Config, OutputVerification, Range};
mod config;
mod opt;
mod output_diff;
static EXIT_CODE: AtomicI32 = AtomicI32::new(0);
#[macro_export]
macro_rules! verbose_println {
($verbosity:expr, $str:expr) => {
if $verbosity {
println!($str);
}
};
($verbosity:expr, $str:expr, $($arg:tt)*) => {
if $verbosity {
println!($str, $($arg)*);
}
};
}
macro_rules! error {
($opt:expr, $fmt:expr, $($args:tt)*) => {
error(std::fmt::format(format_args!($fmt, $($args)*)), $opt.color.should_use_color())
};
}
fn error(text: String, should_use_color: bool) {
eprintln!(
"{}{} {}",
style("error").bold().red().force_styling(should_use_color),
style(":").bold().force_styling(should_use_color),
text
);
EXIT_CODE.store(2, Ordering::SeqCst);
}
enum FormatResult {
Complete,
SuccessBufferedOutput(Vec<u8>),
Diff(Vec<u8>),
}
fn format_file(
path: &Path,
config: Config,
range: Option<Range>,
opt: &opt::Opt,
verify_output: OutputVerification,
) -> Result<FormatResult> {
let contents =
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
let before_formatting = Instant::now();
let formatted_contents = format_code(&contents, config, range, verify_output)
.with_context(|| format!("could not format file {}", path.display()))?;
let after_formatting = Instant::now();
verbose_println!(
opt.verbose,
"formatted {} in {:?}",
path.display(),
after_formatting.duration_since(before_formatting)
);
if opt.check {
let diff = output_diff::output_diff(
&contents,
&formatted_contents,
3,
format!("Diff in {}:", path.display()),
opt.color,
)
.context("failed to create diff")?;
match diff {
Some(diff) => Ok(FormatResult::Diff(diff)),
None => Ok(FormatResult::Complete),
}
} else {
fs::write(path, formatted_contents)
.with_context(|| format!("could not write to {}", path.display()))?;
Ok(FormatResult::Complete)
}
}
fn format_string(
input: String,
config: Config,
range: Option<Range>,
opt: &opt::Opt,
verify_output: OutputVerification,
) -> Result<FormatResult> {
let formatted_contents =
format_code(&input, config, range, verify_output).context("failed to format from stdin")?;
if opt.check {
let diff = output_diff::output_diff(
&input,
&formatted_contents,
3,
"Diff from stdin:".into(),
opt.color,
)
.context("failed to create diff")?;
match diff {
Some(diff) => Ok(FormatResult::Diff(diff)),
None => Ok(FormatResult::Complete),
}
} else {
Ok(FormatResult::SuccessBufferedOutput(
formatted_contents.into_bytes(),
))
}
}
fn format(opt: opt::Opt) -> Result<i32> {
if opt.files.is_empty() {
bail!("no files provided");
}
let config = config::load_config(&opt)?;
let config = config::load_overrides(config, &opt);
verbose_println!(opt.verbose, "config: {:#?}", config);
let range = if opt.range_start.is_some() || opt.range_end.is_some() {
Some(Range::from_values(opt.range_start, opt.range_end))
} else {
None
};
let verify_output = if opt.verify {
OutputVerification::Full
} else {
OutputVerification::None
};
let cwd = std::env::current_dir()?;
let mut walker_builder = WalkBuilder::new(&opt.files[0]);
for file_path in &opt.files[1..] {
walker_builder.add(file_path);
}
walker_builder
.standard_filters(false)
.hidden(true)
.parents(true)
.add_custom_ignore_filename(".styluaignore");
let ignore_path = cwd.join(".styluaignore");
if ignore_path.is_file() {
walker_builder.add_ignore(ignore_path);
}
let use_default_glob = match opt.glob {
Some(ref globs) => {
let mut overrides = OverrideBuilder::new(cwd);
for pattern in globs {
overrides.add(pattern)?;
}
let overrides = overrides.build()?;
walker_builder.overrides(overrides);
false
}
None => true,
};
verbose_println!(
opt.verbose,
"creating a pool with {} threads",
opt.num_threads
);
let pool = ThreadPool::new(std::cmp::max(opt.num_threads, 2)); let (tx, rx) = crossbeam_channel::unbounded();
let opt = Arc::new(opt);
let read_opt = opt.clone();
pool.execute(move || {
for output in rx {
match output {
Ok(result) => match result {
FormatResult::Complete => (),
FormatResult::SuccessBufferedOutput(output) => {
let stdout = stdout();
let mut handle = stdout.lock();
match handle.write_all(&output) {
Ok(_) => (),
Err(err) => {
error!(&read_opt, "could not output to stdout: {:#}", err)
}
};
}
FormatResult::Diff(diff) => {
if EXIT_CODE.load(Ordering::SeqCst) != 2 {
EXIT_CODE.store(1, Ordering::SeqCst);
}
let stdout = stdout();
let mut handle = stdout.lock();
match handle.write_all(&diff) {
Ok(_) => (),
Err(err) => error!(&read_opt, "{:#}", err),
}
}
},
Err(err) => error!(&read_opt, "{:#}", err),
}
}
});
let walker = walker_builder.build();
for result in walker {
match result {
Ok(entry) => {
if entry.is_stdin() {
let tx = tx.clone();
let opt = opt.clone();
pool.execute(move || {
let mut buf = String::new();
match stdin().read_to_string(&mut buf) {
Ok(_) => {
tx.send(format_string(buf, config, range, &opt, verify_output))
}
Err(error) => {
tx.send(Err(error).context("could not format from stdin"))
}
}
.unwrap();
});
} else {
let path = entry.path().to_owned(); let opt = opt.clone();
if path.is_file() {
if use_default_glob && !opt.files.iter().any(|p| path == *p) {
lazy_static::lazy_static! {
static ref DEFAULT_GLOB: globset::GlobSet = {
let mut builder = globset::GlobSetBuilder::new();
builder.add(globset::Glob::new("**/*.lua").expect("cannot create default glob"));
#[cfg(feature = "luau")]
builder.add(globset::Glob::new("**/*.luau").expect("cannot create default luau glob"));
builder.build().expect("cannot build default globset")
};
}
if !DEFAULT_GLOB.is_match(&path) {
continue;
}
}
let tx = tx.clone();
pool.execute(move || {
tx.send(format_file(&path, config, range, &opt, verify_output))
.unwrap()
});
}
}
}
Err(error) => match error {
ignore::Error::WithPath { path, err } => match *err {
ignore::Error::Io(error) => match error.kind() {
std::io::ErrorKind::NotFound => {
error!(
&opt,
"no file or directory found matching '{:#}'",
path.display()
)
}
_ => error!(&opt, "{:#}", error),
},
_ => error!(&opt, "{:#}", err),
},
_ => error!(&opt, "{:#}", error),
},
}
}
drop(tx);
pool.join();
let output_code = if pool.panic_count() > 0 {
2
} else {
EXIT_CODE.load(Ordering::SeqCst)
};
Ok(output_code)
}
fn main() {
let opt = opt::Opt::from_args();
let should_use_color = opt.color.should_use_color();
let exit_code = match format(opt) {
Ok(code) => code,
Err(e) => {
error(format!("{:#}", e), should_use_color);
2
}
};
std::process::exit(exit_code);
}