use std::sync::LazyLock;
use anyhow::Context as _;
use anyhow::anyhow;
use clap::ValueEnum;
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term::Config as TermConfig;
use codespan_reporting::term::DisplayStyle;
use codespan_reporting::term::emit_to_write_style;
use codespan_reporting::term::termcolor::ColorChoice;
use codespan_reporting::term::termcolor::StandardStream;
use serde::Deserialize;
use serde::Serialize;
use wdl_ast::Diagnostic;
static FULL_CONFIG: LazyLock<TermConfig> = LazyLock::new(|| TermConfig {
display_style: DisplayStyle::Rich,
..Default::default()
});
static ONE_LINE_CONFIG: LazyLock<TermConfig> = LazyLock::new(|| TermConfig {
display_style: DisplayStyle::Short,
..Default::default()
});
#[derive(Default, Debug)]
pub struct DiagnosticCounts {
pub errors: usize,
pub warnings: usize,
pub notes: usize,
}
impl DiagnosticCounts {
pub fn verify_no_errors(&self) -> Option<anyhow::Error> {
if self.errors == 0 {
return None;
}
Some(anyhow!(
"failing due to {errors} error{s}",
errors = self.errors,
s = if self.errors == 1 { "" } else { "s" }
))
}
pub fn verify_no_warnings(&self, user_requested: bool) -> Option<anyhow::Error> {
if self.warnings == 0 {
return None;
}
Some(anyhow!(
"failing due to {warnings} warning{s}{cli_note}",
warnings = self.warnings,
s = if self.warnings == 1 { "" } else { "s" },
cli_note = if user_requested {
" (`--deny-warnings` was specified)"
} else {
""
},
))
}
pub fn verify_no_notes(&self, user_requested: bool) -> Option<anyhow::Error> {
if self.notes == 0 {
return None;
}
Some(anyhow!(
"failing due to {notes} note{s}{cli_note}",
notes = self.notes,
s = if self.notes == 1 { "" } else { "s" },
cli_note = if user_requested {
" (`--deny-notes` was specified)"
} else {
""
},
))
}
}
#[derive(Clone, Copy, Debug, Default, ValueEnum, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
#[default]
Full,
OneLine,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Mode::Full => write!(f, "full"),
Mode::OneLine => write!(f, "one-line"),
}
}
}
pub fn get_diagnostics_display_config(
report_mode: Mode,
colorize: bool,
) -> (&'static TermConfig, StandardStream) {
let config = match report_mode {
Mode::Full => &FULL_CONFIG,
Mode::OneLine => &ONE_LINE_CONFIG,
};
let color_choice = if colorize {
ColorChoice::Always
} else {
ColorChoice::Never
};
let stream = StandardStream::stderr(color_choice);
(config, stream)
}
pub fn emit_diagnostics<'a>(
path: &str,
source: String,
diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
report_mode: Mode,
colorize: bool,
) -> anyhow::Result<()> {
let mut files = SimpleFiles::new();
let file_id = files.add(std::borrow::Cow::Borrowed(path), source);
let (config, mut stream) = get_diagnostics_display_config(report_mode, colorize);
for diagnostic in diagnostics {
let diagnostic = diagnostic.to_codespan(file_id);
emit_to_write_style(&mut stream, config, &files, &diagnostic)
.context("failed to emit diagnostic")?;
}
Ok(())
}
#[cfg(feature = "backtrace")]
pub fn emit_diagnostics_with_backtrace<'a>(
path: &str,
source: String,
diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
backtrace: &[wdl_engine::CallLocation],
report_mode: Mode,
colorize: bool,
) -> anyhow::Result<()> {
use std::io::Write;
use codespan_reporting::diagnostic::Label;
use codespan_reporting::diagnostic::LabelStyle;
use wdl_ast::AstNode as _;
const MAX_CALL_LOCATIONS: usize = 10;
let mut map = std::collections::HashMap::new();
let mut files = SimpleFiles::new();
let file_id = files.add(std::borrow::Cow::Borrowed(path), source);
let (config, mut stream) = get_diagnostics_display_config(report_mode, colorize);
for diagnostic in diagnostics {
let diagnostic = diagnostic.to_codespan(file_id).with_labels_iter(
backtrace.iter().take(MAX_CALL_LOCATIONS).map(|l| {
let id = l.document.id();
let file_id = *map.entry(id).or_insert_with(|| {
files.add(l.document.path(), l.document.root().text().to_string())
});
Label {
style: LabelStyle::Secondary,
file_id,
range: l.span.start()..l.span.end(),
message: "called from this location".into(),
}
}),
);
emit_to_write_style(&mut stream, config, &files, &diagnostic)
.context("failed to emit diagnostic")?;
if backtrace.len() > MAX_CALL_LOCATIONS {
writeln!(
&mut stream,
" and {count} more call{s}...",
count = backtrace.len() - MAX_CALL_LOCATIONS,
s = if backtrace.len() - MAX_CALL_LOCATIONS == 1 {
""
} else {
"s"
}
)?;
}
}
Ok(())
}