#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![cfg_attr(not(test), deny(clippy::expect_used))]
#![cfg_attr(not(test), deny(clippy::panic))]
#![allow(clippy::too_many_lines)]
#![allow(clippy::redundant_clone)]
#![allow(clippy::cast_possible_truncation)]
use anyhow::Result;
use clap::Parser;
mod cli;
mod commands;
mod config;
mod discovery;
mod error;
mod io;
mod reporter;
use cli::{Cli, Command};
use error::{ExitCode, format_error};
use io::input::InputOrigin;
use io::{InputSource, OutputWriter};
fn main() {
let exit_code = match run() {
Ok(code) => code,
Err(err) => {
let cli = Cli::parse();
let output_config =
config::OutputConfig::from_cli(cli.quiet, cli.verbose, cli.no_color);
eprintln!("{}", format_error(&err, output_config.use_color()));
ExitCode::ParseError
}
};
std::process::exit(exit_code.as_i32());
}
fn run() -> Result<ExitCode> {
let cli = Cli::parse();
let common_config = config::CommonConfig::from_cli(&cli);
let exit_code = match cli.command {
Some(Command::Parse { file, stats }) => {
let input = InputSource::from_args(file)?;
let cmd = commands::parse::ParseCommand::new(common_config, stats);
cmd.execute(&input)?;
ExitCode::Success
}
Some(Command::Format {
paths,
indent,
width,
jobs,
stdin_files,
include,
exclude,
no_recursive,
dry_run,
strip_comments,
}) => {
let is_batch = is_batch_mode(&paths, stdin_files, &include, &exclude, jobs);
if is_batch {
let mut discovery_config = discovery::DiscoveryConfig::new();
if !include.is_empty() {
discovery_config = discovery_config.with_include_patterns(include);
}
if !exclude.is_empty() {
discovery_config = discovery_config.with_exclude_patterns(exclude);
}
if no_recursive {
discovery_config = discovery_config.with_max_depth(Some(1));
}
let batch_config = commands::format_batch::BatchConfig::new(
common_config
.clone()
.with_formatter(
config::FormatterConfig::new()
.with_indent(indent)
.with_width(width),
)
.with_parallel(config::ParallelConfig::new().with_workers(if jobs == 0 {
None
} else {
Some(jobs)
})),
)
.with_discovery(discovery_config)
.with_dry_run(dry_run)
.with_in_place(cli.in_place);
commands::format_batch::execute_batch(&batch_config, &paths, stdin_files)?
} else if paths.is_empty() {
if cli.in_place {
anyhow::bail!("--in-place (-i) requires a file argument");
}
let input = InputSource::from_stdin()?;
let output = OutputWriter::from_args(cli.output.clone(), false, None)?;
let format_config = common_config.clone().with_formatter(
config::FormatterConfig::new()
.with_indent(indent)
.with_width(width),
);
let cmd = commands::format::FormatCommand::new(format_config, strip_comments);
cmd.execute(&input, &output)?;
ExitCode::Success
} else {
let file_path = &paths[0];
let input = InputSource::from_file(file_path)?;
let output =
OutputWriter::from_args(cli.output.clone(), cli.in_place, Some(file_path))?;
let format_config = common_config.clone().with_formatter(
config::FormatterConfig::new()
.with_indent(indent)
.with_width(width),
);
let cmd = commands::format::FormatCommand::new(format_config, strip_comments);
cmd.execute(&input, &output)?;
ExitCode::Success
}
}
Some(Command::Convert { to, file, pretty }) => {
let input = InputSource::from_args(file)?;
let output =
OutputWriter::from_args(cli.output.clone(), cli.in_place, input.file_path())?;
let cmd = commands::convert::ConvertCommand::new(common_config, to, pretty);
cmd.execute(&input, &output)?;
ExitCode::Success
}
#[cfg(feature = "linter")]
Some(Command::Lint {
paths,
config: config_path,
no_config,
max_line_length,
indent_size,
format,
allow_duplicate_keys,
include,
exclude,
no_recursive,
jobs,
}) => {
if cli.in_place {
anyhow::bail!(
"--in-place is not supported by `fy lint` (auto-fix is not implemented)"
);
}
let is_batch = is_batch_mode(&paths, false, &include, &exclude, jobs);
if is_batch {
let mut discovery_config = discovery::DiscoveryConfig::new();
if !include.is_empty() {
discovery_config = discovery_config.with_include_patterns(include);
}
if !exclude.is_empty() {
discovery_config = discovery_config.with_exclude_patterns(exclude);
}
if no_recursive {
discovery_config = discovery_config.with_max_depth(Some(1));
}
let stdin_fallback = InputSource {
content: String::new(),
origin: InputOrigin::Stdin,
};
let args = commands::lint::LintArgs {
config_path,
no_config,
max_line_length,
indent_size,
format: format.clone(),
allow_duplicate_keys,
};
let cmd = commands::lint::LintCommand::build(
common_config.clone(),
args,
&stdin_fallback,
)?;
let batch_config = commands::lint_batch::LintBatchConfig::new(
common_config.clone().with_parallel(
config::ParallelConfig::new().with_workers(if jobs == 0 {
None
} else {
Some(jobs)
}),
),
cmd.lint_config,
format,
)
.with_discovery(discovery_config);
commands::lint_batch::execute_lint_batch(&batch_config, &paths)?
} else if paths.is_empty() {
let input = InputSource::from_stdin()?;
let args = commands::lint::LintArgs {
config_path,
no_config,
max_line_length,
indent_size,
format,
allow_duplicate_keys,
};
let cmd = commands::lint::LintCommand::build(common_config.clone(), args, &input)?;
cmd.execute(&input)?
} else {
let input = InputSource::from_file(&paths[0])?;
let args = commands::lint::LintArgs {
config_path,
no_config,
max_line_length,
indent_size,
format,
allow_duplicate_keys,
};
let cmd = commands::lint::LintCommand::build(common_config.clone(), args, &input)?;
cmd.execute(&input)?
}
}
None => {
let input = InputSource::from_stdin()?;
let output = OutputWriter::from_args(cli.output.clone(), false, None)?;
let format_config = common_config
.clone()
.with_formatter(config::FormatterConfig::new().with_indent(2).with_width(80));
let cmd = commands::format::FormatCommand::new(format_config, false);
cmd.execute(&input, &output)?;
ExitCode::Success
}
};
Ok(exit_code)
}
fn is_batch_mode(
paths: &[std::path::PathBuf],
stdin_files: bool,
include: &[String],
exclude: &[String],
jobs: usize,
) -> bool {
if stdin_files {
return true;
}
if paths.len() > 1 {
return true;
}
if paths.len() == 1 && is_batch_path(&paths[0]) {
return true;
}
if !include.is_empty() || !exclude.is_empty() {
return true;
}
if jobs > 0 {
return true;
}
false
}
fn is_batch_path(path: &std::path::Path) -> bool {
path.is_dir() || contains_glob_chars(&path.to_string_lossy())
}
fn contains_glob_chars(s: &str) -> bool {
s.contains('*') || s.contains('?') || s.contains('[')
}