1use std::sync::LazyLock;
4
5use anyhow::Context as _;
6use anyhow::anyhow;
7use clap::ValueEnum;
8use codespan_reporting::files::SimpleFiles;
9use codespan_reporting::term::Config as TermConfig;
10use codespan_reporting::term::DisplayStyle;
11use codespan_reporting::term::emit_to_write_style;
12use codespan_reporting::term::termcolor::ColorChoice;
13use codespan_reporting::term::termcolor::StandardStream;
14use serde::Deserialize;
15use serde::Serialize;
16use wdl_ast::Diagnostic;
17
18static FULL_CONFIG: LazyLock<TermConfig> = LazyLock::new(|| TermConfig {
20 display_style: DisplayStyle::Rich,
21 ..Default::default()
22});
23
24static ONE_LINE_CONFIG: LazyLock<TermConfig> = LazyLock::new(|| TermConfig {
26 display_style: DisplayStyle::Short,
27 ..Default::default()
28});
29
30#[derive(Default, Debug)]
32pub struct DiagnosticCounts {
33 pub errors: usize,
35 pub warnings: usize,
37 pub notes: usize,
39}
40
41impl DiagnosticCounts {
42 pub fn verify_no_errors(&self) -> Option<anyhow::Error> {
44 if self.errors == 0 {
45 return None;
46 }
47
48 Some(anyhow!(
49 "failing due to {errors} error{s}",
50 errors = self.errors,
51 s = if self.errors == 1 { "" } else { "s" }
52 ))
53 }
54
55 pub fn verify_no_warnings(&self, user_requested: bool) -> Option<anyhow::Error> {
57 if self.warnings == 0 {
58 return None;
59 }
60
61 Some(anyhow!(
62 "failing due to {warnings} warning{s}{cli_note}",
63 warnings = self.warnings,
64 s = if self.warnings == 1 { "" } else { "s" },
65 cli_note = if user_requested {
66 " (`--deny-warnings` was specified)"
67 } else {
68 ""
69 },
70 ))
71 }
72
73 pub fn verify_no_notes(&self, user_requested: bool) -> Option<anyhow::Error> {
75 if self.notes == 0 {
76 return None;
77 }
78
79 Some(anyhow!(
80 "failing due to {notes} note{s}{cli_note}",
81 notes = self.notes,
82 s = if self.notes == 1 { "" } else { "s" },
83 cli_note = if user_requested {
84 " (`--deny-notes` was specified)"
85 } else {
86 ""
87 },
88 ))
89 }
90}
91
92#[derive(Clone, Copy, Debug, Default, ValueEnum, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum Mode {
96 #[default]
98 Full,
99
100 OneLine,
102}
103
104impl std::fmt::Display for Mode {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Mode::Full => write!(f, "full"),
108 Mode::OneLine => write!(f, "one-line"),
109 }
110 }
111}
112
113pub fn get_diagnostics_display_config(
115 report_mode: Mode,
116 colorize: bool,
117) -> (&'static TermConfig, StandardStream) {
118 let config = match report_mode {
119 Mode::Full => &FULL_CONFIG,
120 Mode::OneLine => &ONE_LINE_CONFIG,
121 };
122
123 let color_choice = if colorize {
124 ColorChoice::Always
125 } else {
126 ColorChoice::Never
127 };
128
129 let stream = StandardStream::stderr(color_choice);
130
131 (config, stream)
132}
133
134pub fn emit_diagnostics<'a>(
136 path: &str,
137 source: &str,
138 diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
139 report_mode: Mode,
140 colorize: bool,
141) -> anyhow::Result<()> {
142 use std::borrow::Cow;
143
144 let mut files = SimpleFiles::new();
145
146 let file_id = files.add(Cow::Borrowed(path), Cow::Borrowed(source));
147
148 let (config, mut stream) = get_diagnostics_display_config(report_mode, colorize);
149
150 for diagnostic in diagnostics {
151 let diagnostic = diagnostic.to_codespan(file_id);
152 emit_to_write_style(&mut stream, config, &files, &diagnostic)
153 .context("failed to emit diagnostic")?;
154 }
155
156 Ok(())
157}
158
159#[cfg(feature = "backtrace")]
162pub fn emit_diagnostics_with_backtrace<'a>(
163 path: &str,
164 source: &str,
165 diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
166 backtrace: &[wdl_engine::CallLocation],
167 report_mode: Mode,
168 colorize: bool,
169) -> anyhow::Result<()> {
170 use std::borrow::Cow;
171 use std::io::Write;
172
173 use codespan_reporting::diagnostic::Label;
174 use codespan_reporting::diagnostic::LabelStyle;
175 use wdl_ast::AstNode as _;
176
177 const MAX_CALL_LOCATIONS: usize = 10;
179
180 let mut map = std::collections::HashMap::new();
181 let mut files = SimpleFiles::new();
182
183 let file_id = files.add(Cow::Borrowed(path), Cow::Borrowed(source));
184
185 let (config, mut stream) = get_diagnostics_display_config(report_mode, colorize);
186
187 for diagnostic in diagnostics {
188 let diagnostic = diagnostic.to_codespan(file_id).with_labels_iter(
189 backtrace.iter().take(MAX_CALL_LOCATIONS).map(|l| {
190 let id = l.document.id();
191 let file_id = *map.entry(id).or_insert_with(|| {
192 files.add(
193 l.document.path(),
194 Cow::Owned(l.document.root().text().to_string()),
195 )
196 });
197
198 Label {
199 style: LabelStyle::Secondary,
200 file_id,
201 range: l.span.start()..l.span.end(),
202 message: "called from this location".into(),
203 }
204 }),
205 );
206
207 emit_to_write_style(&mut stream, config, &files, &diagnostic)
208 .context("failed to emit diagnostic")?;
209
210 if backtrace.len() > MAX_CALL_LOCATIONS {
211 writeln!(
212 &mut stream,
213 " and {count} more call{s}...",
214 count = backtrace.len() - MAX_CALL_LOCATIONS,
215 s = if backtrace.len() - MAX_CALL_LOCATIONS == 1 {
216 ""
217 } else {
218 "s"
219 }
220 )?;
221 }
222 }
223
224 Ok(())
225}