use std::path::{Path, PathBuf};
use std::process::ExitCode;
use bynk_emit::project::{
self, CompileOptions, ProjectPathsError, read_project_paths, try_read_project_paths,
};
use bynk_fmt::{FormatOptions, format_source};
pub mod coverage;
pub mod probe;
pub mod test_json;
pub mod test_runner;
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 try_project_options(input: &Path) -> Result<CompileOptions, ProjectPathsError> {
if input.join("bynk.toml").exists() || input.join("src").is_dir() {
Ok(CompileOptions::split(
input.to_path_buf(),
try_read_project_paths(input)?,
))
} else {
Ok(CompileOptions::single(input.to_path_buf()))
}
}
pub fn print_project_failure(failure: &project::ProjectFailure) {
for ae in &failure.errors {
match attributed_snapshot(ae, &failure.snapshots) {
Some((label, text)) => {
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}");
}
for (_, label) in &ae.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn print_project_warnings(
warnings: &[project::AttributedError],
snapshots: &[(PathBuf, String)],
) {
for w in warnings {
match attributed_snapshot(w, snapshots) {
Some((label, text)) => {
bynk_render::print_errors(std::slice::from_ref(&w.error), text, &label)
}
None => {
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}");
}
for (_, label) in &w.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn print_project_warnings_short(
warnings: &[project::AttributedError],
snapshots: &[(PathBuf, String)],
) {
for w in warnings {
match attributed_snapshot(w, snapshots) {
Some((label, text)) => eprintln!("{}", bynk_render::short_line(&label, text, &w.error)),
None => eprintln!(
"{}[{}]: {}",
bynk_render::severity_word(&w.error),
w.error.category,
w.error.message
),
}
}
}
fn attributed_snapshot<'a>(
ae: &project::AttributedError,
snapshots: &'a [(PathBuf, String)],
) -> Option<(String, &'a str)> {
let path = ae.source_path.as_deref()?;
let text = snapshots
.iter()
.find(|(p, _)| p.as_path() == path)
.map(|(_, t)| t.as_str())?;
Some((path.to_string_lossy().replace('\\', "/"), text))
}
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> {
failure
.errors
.iter()
.map(|ae| match attributed_snapshot(ae, &failure.snapshots) {
Some((label, text)) => 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 print_project_check(check: &project::ProjectCheck) {
for ae in &check.errors {
match attributed_snapshot(ae, &check.snapshots) {
Some((label, text)) => {
bynk_render::print_errors(std::slice::from_ref(&ae.error), text, &label);
}
None => {
eprintln!(
"{}[{}]: {}",
bynk_render::severity_word(&ae.error),
ae.error.category,
ae.error.message
);
for note in &ae.error.notes {
eprintln!(" note: {note}");
}
for (_, label) in &ae.error.labels {
eprintln!(" label: {label}");
}
}
}
}
}
pub fn project_check_short_lines(check: &project::ProjectCheck) -> Vec<String> {
check
.errors
.iter()
.map(|ae| match attributed_snapshot(ae, &check.snapshots) {
Some((label, text)) => 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 print_project_check_short(check: &project::ProjectCheck) {
for line in project_check_short_lines(check) {
eprintln!("{line}");
}
}
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() {
let options = match try_project_options(input) {
Ok(o) => o,
Err(e) => {
eprintln!("{prog}: {e}");
return ExitCode::FAILURE;
}
};
let check = project::check_project(&options);
let has_errors = check.has_errors();
if short {
print_project_check_short(&check);
} else {
print_project_check(&check);
}
if has_errors {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
} 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
}
}
}
}