use std::path::{Path, PathBuf};
use std::process::ExitCode;
use bynk_emit::project::{self, CompileOptions, read_project_paths};
use bynk_fmt::{FormatOptions, format_source};
pub fn project_options(input: &Path) -> CompileOptions {
if input.join("bynk.toml").exists() || input.join("src").is_dir() {
CompileOptions::split(input.to_path_buf(), read_project_paths(input))
} else {
CompileOptions::single(input.to_path_buf())
}
}
pub fn print_project_failure(failure: &project::ProjectFailure) {
let texts: std::collections::HashMap<&Path, &str> = failure
.snapshots
.iter()
.map(|(p, t)| (p.as_path(), t.as_str()))
.collect();
for ae in &failure.errors {
match ae
.source_path
.as_deref()
.and_then(|p| texts.get(p).map(|t| (p, *t)))
{
Some((path, text)) => {
let label = path.to_string_lossy().replace('\\', "/");
bynk_render::print_errors(std::slice::from_ref(&ae.error), text, &label);
}
None => {
eprintln!("[{}] {}", ae.error.category, ae.error.message);
for note in &ae.error.notes {
eprintln!(" note: {note}");
}
}
}
}
}
pub fn print_project_warnings(warnings: &[project::AttributedError]) {
for w in warnings {
let where_ = w
.source_path
.as_deref()
.map(|p| format!("{}: ", p.to_string_lossy().replace('\\', "/")))
.unwrap_or_default();
eprintln!("{where_}warning[{}]: {}", w.error.category, w.error.message);
for note in &w.error.notes {
eprintln!(" note: {note}");
}
}
}
pub fn print_project_failure_short(failure: &project::ProjectFailure) {
for line in project_failure_short_lines(failure) {
eprintln!("{line}");
}
}
pub fn project_failure_short_lines(failure: &project::ProjectFailure) -> Vec<String> {
let texts: std::collections::HashMap<&Path, &str> = failure
.snapshots
.iter()
.map(|(p, t)| (p.as_path(), t.as_str()))
.collect();
failure
.errors
.iter()
.map(|ae| {
match ae
.source_path
.as_deref()
.and_then(|p| texts.get(p).map(|t| (p, *t)))
{
Some((path, text)) => {
let label = path.to_string_lossy().replace('\\', "/");
bynk_render::short_line(&label, text, &ae.error)
}
None => format!(
"{}[{}]: {}",
bynk_render::severity_word(&ae.error),
ae.error.category,
ae.error.message
),
}
})
.collect()
}
pub fn run_fmt(prog: &str, inputs: &[PathBuf], check: bool) -> ExitCode {
let opts = FormatOptions::default();
if inputs.is_empty() {
eprintln!("{prog} fmt: no input files (pass file paths or `-` for stdin)");
return ExitCode::FAILURE;
}
let mut had_diff = false;
let mut had_error = false;
for input in inputs {
if input.as_os_str() == "-" {
use std::io::Read;
let mut source = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut source) {
eprintln!("{prog} fmt: read from stdin: {e}");
return ExitCode::FAILURE;
}
match format_source(&source, &opts) {
Ok(formatted) => {
if check {
if formatted != source {
eprintln!("{prog} fmt: <stdin> is not canonically formatted");
had_diff = true;
}
} else {
print!("{formatted}");
}
}
Err(e) => {
bynk_render::print_errors(&e.errors, &source, "<stdin>");
return ExitCode::FAILURE;
}
}
continue;
}
let source = match std::fs::read_to_string(input) {
Ok(s) => s,
Err(e) => {
eprintln!("{prog} fmt: read `{}`: {e}", input.display());
had_error = true;
continue;
}
};
let filename = input.display().to_string();
match format_source(&source, &opts) {
Ok(formatted) => {
if check {
if formatted != source {
eprintln!(
"{prog} fmt: {} is not canonically formatted",
input.display()
);
had_diff = true;
}
} else if formatted != source
&& let Err(e) = atomic_write(input, &formatted)
{
eprintln!("{prog} fmt: write `{}`: {e}", input.display());
had_error = true;
}
}
Err(e) => {
bynk_render::print_errors(&e.errors, &source, &filename);
had_error = true;
}
}
}
if had_error || (check && had_diff) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn atomic_write(path: &Path, contents: &str) -> std::io::Result<()> {
use std::io::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = path.parent().filter(|p| !p.as_os_str().is_empty());
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let (mut file, tmp) = loop {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp_name = format!(".{file_name}.bynk-fmt.{}.{n}.tmp", std::process::id());
let tmp = match dir {
Some(d) => d.join(tmp_name),
None => PathBuf::from(tmp_name),
};
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
{
Ok(f) => break (f, tmp),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
};
let write_result = (|| {
if let Ok(meta) = std::fs::metadata(path) {
let _ = file.set_permissions(meta.permissions());
}
file.write_all(contents.as_bytes())?;
file.sync_all()
})();
if let Err(e) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
Ok(())
}
pub fn run_check(prog: &str, input: &Path, short: bool) -> ExitCode {
if input.is_dir() {
match project::compile_project(&project_options(input)) {
Ok(out) => {
print_project_warnings(&out.warnings);
ExitCode::SUCCESS
}
Err(failure) => {
if short {
print_project_failure_short(&failure);
} else {
print_project_failure(&failure);
}
ExitCode::FAILURE
}
}
} else {
let source = match std::fs::read_to_string(input) {
Ok(s) => s,
Err(e) => {
eprintln!("{prog}: could not read `{}`: {e}", input.display());
return ExitCode::FAILURE;
}
};
let filename = input.display().to_string();
match bynk_emit::compile_with_warnings(&source, &filename) {
Ok(compiled) => {
if !compiled.warnings.is_empty() {
if short {
bynk_render::print_errors_short(&compiled.warnings, &source, &filename);
} else {
bynk_render::print_errors(&compiled.warnings, &source, &filename);
}
}
ExitCode::SUCCESS
}
Err(errors) => {
if short {
bynk_render::print_errors_short(&errors, &source, &filename);
} else {
bynk_render::print_errors(&errors, &source, &filename);
}
ExitCode::FAILURE
}
}
}
}