use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use clap::Args;
use nova_fmt::{FileError, FormatError};
#[derive(Args)]
pub struct FmtCmd {
paths: Vec<PathBuf>,
#[arg(long)]
check: bool,
#[arg(long, conflicts_with = "paths")]
stdin: bool,
}
pub fn run(cmd: FmtCmd) -> i32 {
let code = if cmd.stdin {
stdin(cmd.check)
} else {
files(&cmd.paths, cmd.check)
};
let _ = std::io::stdout().flush();
code
}
fn stdin(check: bool) -> i32 {
let mut source = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut source) {
eprintln!("error: reading standard input: {e}");
return 2;
}
match nova_fmt::format_text(&source) {
Ok(out) if check => i32::from(out != source),
Ok(out) => {
print!("{out}");
0
}
Err(e) => {
report("<stdin>", &e);
2
}
}
}
fn files(paths: &[PathBuf], check: bool) -> i32 {
let files = match collect(paths) {
Ok(files) => files,
Err(message) => {
eprintln!("error: {message}");
return 2;
}
};
let mut code = 0;
for file in &files {
let shown = file.display().to_string();
match nova_fmt::format_file(file) {
Ok(f) if !f.changed() => {}
Ok(_) if check => {
println!("would reformat: {shown}");
code = code.max(1);
}
Ok(f) => {
if let Err(e) = write(file, &f.formatted) {
eprintln!("error: writing {shown}: {e}");
code = 2;
}
}
Err(FileError::Io(e)) => {
eprintln!("error: reading {shown}: {e}");
code = 2;
}
Err(FileError::NotUtf8) => {
eprintln!("error: {shown} is not UTF-8, so it was left unchanged");
code = 2;
}
Err(FileError::Format(e)) => {
report(&shown, &e);
code = 2;
}
}
}
code
}
fn report(file: &str, e: &FormatError) {
match e {
FormatError::Syntax { rendered, .. } => eprint!("{rendered}"),
FormatError::Internal { first_difference } => eprintln!(
"error: nova fmt could not format {file} safely, so it was left unchanged: \
{first_difference}"
),
}
}
fn collect(paths: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
let mut files = Vec::new();
if paths.is_empty() {
let cwd =
std::env::current_dir().map_err(|e| format!("reading the current directory: {e}"))?;
let (src, tests) = match nova_pm::find_root(&cwd) {
Some(root) => (root.join("src"), Some(root.join("tests"))),
None if Path::new("src/main.nova").is_file() => (PathBuf::from("src"), None),
None => {
return Err("no project here, and no src/main.nova: \
name the files or directories to format"
.to_owned())
}
};
search(&src, &mut files, true).map_err(|e| format!("searching {}: {e}", src.display()))?;
if let Some(tests) = tests.filter(|tests| tests.is_dir()) {
search(&tests, &mut files, true)
.map_err(|e| format!("searching {}: {e}", tests.display()))?;
}
}
for path in paths {
if path.is_dir() {
search(path, &mut files, false)
.map_err(|e| format!("searching {}: {e}", path.display()))?;
} else if path.is_file() {
files.push(path.clone());
} else {
return Err(format!("{} does not exist", path.display()));
}
}
files.sort();
files.dedup();
Ok(files)
}
fn search(dir: &Path, files: &mut Vec<PathBuf>, skip_packages: bool) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let kind = entry.file_type()?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
if kind.is_dir() {
let package = skip_packages && path.join(nova_pm::MANIFEST).is_file();
if name != "target" && !name.starts_with('.') && !package {
search(&path, files, skip_packages)?;
}
} else if name.ends_with(".nova") && (kind.is_file() || path.is_file()) {
files.push(path);
}
}
Ok(())
}
fn write(file: &Path, text: &str) -> std::io::Result<()> {
let target = std::fs::canonicalize(file)?;
let permissions = std::fs::metadata(&target)?.permissions();
if permissions.readonly() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"the file is read-only, so it was left unchanged",
));
}
let name = target
.file_name()
.map_or_else(String::new, |n| n.to_string_lossy().into_owned());
let tmp = target.with_file_name(format!(".{name}.nova-fmt.tmp"));
let _ = std::fs::remove_file(&tmp);
replace(&tmp, &target, text, permissions).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp);
})
}
fn replace(
tmp: &Path,
target: &Path,
text: &str,
permissions: std::fs::Permissions,
) -> std::io::Result<()> {
let mut out = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(tmp)?;
out.write_all(text.as_bytes())?;
out.sync_all()?;
drop(out);
std::fs::set_permissions(tmp, permissions)?;
std::fs::rename(tmp, target)
}