use std::io::{IsTerminal, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use rayon::prelude::*;
use fatou::cli::{
Cli, ColorChoice, Commands, DebugChecksArg, DebugCommand, LintOutput, ParseFormat,
};
use fatou::config::Config;
use fatou::debug::{
CheckKind, DebugArtifacts, DebugFailure, build_debug_report, checks_label,
run_debug_checks_for_file, sanitize_path_for_filename, write_debug_artifacts,
};
use fatou::file_discovery::ExcludeFilter;
use fatou::formatter::{self, FormatStyle};
use fatou::linter::{self, LintStatus, OutputMode};
use fatou::parser::{parse, reconstruct, to_juliasyntax_sexpr};
fn main() -> ExitCode {
env_logger::init();
let cli = Cli::parse();
match run(cli) {
Ok(code) => code,
Err(message) => {
eprintln!("error: {message}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> Result<ExitCode, String> {
match cli.command {
Commands::Parse {
file,
quiet,
verify,
to,
} => run_parse(file, quiet, verify, to),
Commands::Format {
paths,
check,
line_width,
indent_width,
exclude,
force_exclude,
} => {
let (config, config_path) = load_config(&cli.config, cli.no_config)?;
let style = style_with_overrides(&config, line_width, indent_width);
let filter =
resolve_exclude_filter(&config, config_path.as_deref(), &exclude, force_exclude)?;
run_format(paths, check, style, &filter)
}
Commands::Lint {
paths,
fix,
unsafe_fixes,
exclude,
force_exclude,
output,
} => {
let (config, config_path) = load_config(&cli.config, cli.no_config)?;
let filter =
resolve_exclude_filter(&config, config_path.as_deref(), &exclude, force_exclude)?;
run_lint(
paths,
output,
fix,
unsafe_fixes,
cli.color,
&config,
&filter,
)
}
Commands::Lsp => fatou::lsp::run()
.map(|()| ExitCode::SUCCESS)
.map_err(|err| err.to_string()),
Commands::Debug { command } => match command {
DebugCommand::Format {
paths,
checks,
report,
dump_dir,
dump_passes,
exclude,
force_exclude,
} => {
let (config, config_path) = load_config(&cli.config, cli.no_config)?;
let style = style_with_overrides(&config, None, None);
let filter = resolve_exclude_filter(
&config,
config_path.as_deref(),
&exclude,
force_exclude,
)?;
run_debug_format(
&paths,
checks,
report,
dump_dir.as_deref(),
dump_passes,
style,
&filter,
)
}
},
}
}
fn run_debug_format(
paths: &[PathBuf],
checks: DebugChecksArg,
report: bool,
dump_dir: Option<&Path>,
dump_passes: bool,
style: FormatStyle,
exclude: &ExcludeFilter,
) -> Result<ExitCode, String> {
if paths.is_empty() {
eprintln!("fatou: debug format requires at least one file or directory");
return Ok(ExitCode::from(2));
}
let files =
fatou::file_discovery::collect_julia_files(paths, exclude).map_err(|e| e.to_string())?;
if files.is_empty() {
if exclude.force() {
return Ok(ExitCode::SUCCESS);
}
return Err("no .jl files found under the provided input paths".to_string());
}
let outcomes: Vec<(String, Result<DebugArtifacts, String>)> = files
.par_iter()
.map(|path| {
let label = path.display().to_string();
let outcome = match std::fs::read_to_string(path) {
Ok(content) => Ok(run_debug_checks_for_file(&content, style, checks)),
Err(err) => Err(format!("fatou: cannot read {label}: {err}")),
};
(label, outcome)
})
.collect();
let mut files_checked = 0usize;
let mut io_failed = false;
let mut collected: Vec<(String, DebugFailure)> = Vec::new();
for (label, outcome) in outcomes {
match outcome {
Err(msg) => {
eprintln!("{msg}");
io_failed = true;
}
Ok(artifacts) => {
files_checked += 1;
if let Some(dir) = dump_dir {
let stem = sanitize_path_for_filename(&label);
if let Err(err) = write_debug_artifacts(dir, &stem, &artifacts, dump_passes) {
eprintln!(
"fatou: cannot write debug artifacts to {}: {err}",
dir.display()
);
io_failed = true;
}
}
for failure in artifacts.failures {
if !report {
eprintln!("Debug check failed ({}) in {label}", failure.kind.label());
if failure.kind == CheckKind::FormatError {
eprintln!(" {}", failure.left);
}
}
collected.push((label.clone(), failure));
}
}
}
}
if report {
print!("{}", build_debug_report(checks, files_checked, &collected));
} else if collected.is_empty() && !io_failed {
println!(
"All checks passed (checks: {}, files: {files_checked})",
checks_label(checks)
);
}
Ok(if collected.is_empty() && !io_failed {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
fn run_parse(
file: Option<PathBuf>,
quiet: bool,
verify: bool,
to: ParseFormat,
) -> Result<ExitCode, String> {
let text = read_source(file.as_deref())?;
let output = parse(&text);
if !quiet {
match to {
ParseFormat::Cst => print!("{:#?}", output.cst),
ParseFormat::Sexpr => {
println!("{}", to_juliasyntax_sexpr(&output.cst, &output.diagnostics))
}
}
for diag in &output.diagnostics {
eprintln!(
"diagnostic [{}..{}]: {}",
diag.start, diag.end, diag.message
);
}
}
if verify {
let reconstructed = reconstruct(&text);
if reconstructed == text {
eprintln!("losslessness OK");
} else {
eprintln!("losslessness FAILED: reconstruction differs from input");
return Ok(ExitCode::FAILURE);
}
}
Ok(ExitCode::SUCCESS)
}
fn run_format(
paths: Vec<PathBuf>,
check: bool,
style: FormatStyle,
exclude: &ExcludeFilter,
) -> Result<ExitCode, String> {
if paths.is_empty() {
let text = read_source(None)?;
let formatted = formatter::format_with_style(&text, style).map_err(|e| e.to_string())?;
print!("{formatted}");
return Ok(ExitCode::SUCCESS);
}
if check {
let result = formatter::check_paths(&paths, style, exclude).map_err(|e| e.to_string())?;
for changed in &result.changed {
println!("would reformat {}", changed.path.display());
print!("{}", changed.diff);
}
return Ok(if result.changed.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
});
}
let files =
fatou::file_discovery::collect_julia_files(&paths, exclude).map_err(|e| e.to_string())?;
files.par_iter().try_for_each(|path| {
let original = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let formatted =
formatter::format_with_style(&original, style).map_err(|e| e.to_string())?;
if formatted != original {
std::fs::write(path, formatted).map_err(|e| e.to_string())?;
}
Ok::<(), String>(())
})?;
Ok(ExitCode::SUCCESS)
}
fn run_lint(
paths: Vec<PathBuf>,
output: LintOutput,
fix: bool,
unsafe_fixes: bool,
color: ColorChoice,
config: &Config,
exclude: &ExcludeFilter,
) -> Result<ExitCode, String> {
if paths.is_empty() {
return Err("lint requires at least one path".to_string());
}
let mode = match output {
LintOutput::Pretty => OutputMode::Pretty,
LintOutput::Concise => OutputMode::Concise,
LintOutput::Json => OutputMode::Json,
};
if fix || unsafe_fixes {
return run_lint_fix(paths, mode, unsafe_fixes, color, config, exclude);
}
let use_color = color_enabled(color, std::io::stderr().is_terminal());
let result = linter::check_paths_with_config(&paths, &config.lint, exclude)
.map_err(|e| e.to_string())?;
warn_unknown_rules(&result.unknown_rules);
let diagnostics: Vec<_> = result
.reports
.iter()
.flat_map(|report| report.diagnostics.clone())
.collect();
let rendered = linter::render_findings(&diagnostics, mode, use_color, &|path| {
path.and_then(|p| std::fs::read_to_string(p).ok())
});
emit(mode, &rendered);
let has_parse_errors = result
.reports
.iter()
.any(|report| matches!(report.status, LintStatus::ParseDiagnostics { .. }));
if result.total_findings > 0 || has_parse_errors {
Ok(ExitCode::FAILURE)
} else {
eprintln!("checked {} file(s): clean", result.checked_files);
Ok(ExitCode::SUCCESS)
}
}
fn run_lint_fix(
paths: Vec<PathBuf>,
mode: OutputMode,
unsafe_fixes: bool,
color: ColorChoice,
config: &Config,
exclude: &ExcludeFilter,
) -> Result<ExitCode, String> {
let use_color = color_enabled(color, std::io::stderr().is_terminal());
let (_, unknown_rules) = linter::ResolvedRules::resolve(&config.lint);
warn_unknown_rules(&unknown_rules);
let files =
fatou::file_discovery::collect_julia_files(&paths, exclude).map_err(|e| e.to_string())?;
let outcomes = files
.par_iter()
.map(|path| {
let original = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let outcome = linter::fix_source(Some(path), &original, &config.lint, unsafe_fixes);
let changed = outcome.output != original;
if changed {
std::fs::write(path, &outcome.output).map_err(|e| e.to_string())?;
}
Ok::<_, String>((outcome.applied, changed, outcome.remaining))
})
.collect::<Result<Vec<_>, String>>()?;
let mut applied = 0usize;
let mut changed_files = 0usize;
let mut remaining = Vec::new();
for (file_applied, changed, file_remaining) in outcomes {
applied += file_applied;
if changed {
changed_files += 1;
}
remaining.extend(file_remaining);
}
let rendered = linter::render_findings(&remaining, mode, use_color, &|path| {
path.and_then(|p| std::fs::read_to_string(p).ok())
});
emit(mode, &rendered);
if applied > 0 {
eprintln!("fixed {applied} issue(s) in {changed_files} file(s)");
}
if remaining.is_empty() {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::FAILURE)
}
}
fn warn_unknown_rules(unknown: &[String]) {
for id in unknown {
eprintln!("warning: unknown rule `{id}` in select/ignore/severity");
}
}
fn emit(mode: OutputMode, rendered: &str) {
if matches!(mode, OutputMode::Json) {
print!("{rendered}");
} else {
eprint!("{rendered}");
}
}
fn color_enabled(choice: ColorChoice, is_terminal: bool) -> bool {
match choice {
ColorChoice::Always => true,
ColorChoice::Never => false,
ColorChoice::Auto => std::env::var_os("NO_COLOR").is_none() && is_terminal,
}
}
fn style_with_overrides(
config: &Config,
line_width: Option<u32>,
indent_width: Option<u32>,
) -> FormatStyle {
let mut style = FormatStyle::from(&config.format);
if let Some(width) = line_width {
style.line_width = width;
}
if let Some(width) = indent_width {
style.indent_width = width;
}
style
}
fn resolve_exclude_filter(
config: &Config,
config_path: Option<&Path>,
cli_patterns: &[String],
force: bool,
) -> Result<ExcludeFilter, String> {
let anchor = std::env::current_dir().map_err(|e| e.to_string())?;
let filter = config
.exclude_filter(config_path, &anchor, cli_patterns)
.map_err(|e| e.to_string())?;
Ok(filter.with_force_exclude(force))
}
fn load_config(
explicit_config: &Option<PathBuf>,
no_config: bool,
) -> Result<(Config, Option<PathBuf>), String> {
let anchor = std::env::current_dir().map_err(|e| e.to_string())?;
let (config, path, warnings) = Config::resolve(explicit_config.as_deref(), no_config, &anchor)
.map_err(|e| e.to_string())?;
for warning in &warnings {
eprintln!("warning: {warning}");
}
Ok((config, path))
}
fn read_source(path: Option<&Path>) -> Result<String, String> {
match path {
Some(path) => std::fs::read_to_string(path).map_err(|e| e.to_string()),
None => {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.map_err(|e| e.to_string())?;
Ok(buffer)
}
}
}