1use std::{
4 borrow::Borrow,
5 collections::BTreeMap,
6 fmt::{self, Write as _},
7 io::Write as _,
8};
9
10use miette::{
11 Diagnostic, GraphicalReportHandler, GraphicalTheme, LabeledSpan, MietteError,
12 MietteSpanContents, Severity, SourceCode, SourceSpan, SpanContents,
13};
14use owo_colors::Style;
15use tap::{Pipe, Tap, TapFallible};
16use tracing::{Level, debug, error, info, level_filters::LevelFilter, trace, warn};
17
18use crate::{
19 emit_debug,
20 env::{is_colored, is_logging},
21 error::{ExpectFmt, put_severity},
22 logging::stderr,
23};
24
25pub trait IssueItem: Send + Sync {
31 type Kind: Issue;
32 fn issue(&self) -> Self::Kind;
33 fn label(&self) -> LabeledSpan;
34}
35
36pub trait Issue: fmt::Debug + Default + Clone + Send + Sync {
38 fn title(&self) -> impl fmt::Display;
39 fn level(&self) -> Level;
40}
41
42pub struct Diagnostics<'a, K, P> {
44 text: &'a str,
45 name: K,
46 issues: Vec<P>,
47}
48
49impl<K, P> Diagnostics<'_, K, P>
50where
51 K: Title,
52 P: IssueItem,
53{
54 pub fn to_report(&self) -> String {
56 let handler = if is_colored() {
57 GraphicalTheme::unicode()
58 } else {
59 GraphicalTheme::unicode_nocolor()
60 }
61 .tap_mut(|t| t.characters.error = "error:".into())
62 .tap_mut(|t| t.characters.warning = "warning:".into())
63 .tap_mut(|t| t.characters.advice = "info:".into())
64 .tap_mut(|t| t.styles.advice = Style::new().green().stderr())
65 .tap_mut(|t| t.styles.warning = Style::new().yellow().stderr())
66 .tap_mut(|t| t.styles.error = Style::new().red().stderr())
67 .tap_mut(|t| {
68 t.styles.highlights = if is_colored() {
71 self.issues
72 .iter()
73 .map(|item| level_style(item.issue().level()))
74 .collect()
75 } else {
76 vec![Style::new()]
77 }
78 })
79 .pipe(GraphicalReportHandler::new_themed);
80
81 let mut output = String::new();
82 handler.render_report(&mut output, self).expect_fmt();
83 output
84 }
85
86 pub fn to_traces(&self) {
87 for item in self.issues.iter() {
88 let issue = item.issue();
89 let label = item.label();
90 let source = self
91 .read_span(label.inner(), 0, 0)
92 .expect("self.read_span infallible");
93 let path = source.name().unwrap_or("<anonymous>");
94 let line = source.line() + 1;
95 let column = source.column() + 1;
96 let title = issue.title();
97 let level = issue.level();
98 let label = label.label().unwrap_or_default();
99 let message = format_args!("{path}:{line}:{column}: {title}");
100 let message = if label.is_empty() {
101 message
102 } else {
103 format_args!("{message}: {label}")
104 };
105 if level >= Level::TRACE {
106 trace!("{message}")
107 } else if level >= Level::DEBUG {
108 debug!("{message}")
109 } else if level >= Level::INFO {
110 info!("{message}")
111 } else if level >= Level::WARN {
112 warn!("{message}")
113 } else {
114 error!("{message}")
115 }
116 }
117 }
118}
119
120impl<'a, K, P> Diagnostics<'a, K, P>
121where
122 P: IssueItem,
123{
124 pub fn new(text: &'a str, name: K, issues: Vec<P>) -> Self {
125 Self { text, name, issues }
126 }
127
128 pub fn name(&self) -> &K {
129 &self.name
130 }
131
132 fn status(&self) -> P::Kind {
133 self.issues
134 .iter()
135 .map(|p| p.issue())
136 .min_by_key(|s| s.level())
137 .unwrap_or_default()
138 }
139}
140
141impl<K, P> Diagnostic for Diagnostics<'_, K, P>
142where
143 K: Title,
144 P: IssueItem,
145{
146 fn severity(&self) -> Option<Severity> {
147 match self.status().level() {
148 Level::ERROR => Some(Severity::Error),
149 Level::WARN => Some(Severity::Warning),
150 _ => Some(Severity::Advice),
151 }
152 }
153
154 fn source_code(&self) -> Option<&dyn SourceCode> {
155 Some(self)
156 }
157
158 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
159 Some(Box::new(self.issues.iter().map(|p| p.label())))
160 }
161
162 fn help(&self) -> Option<Box<dyn fmt::Display + '_>> {
163 if self.issues.is_empty() {
166 Some(Box::new(format!("in {}", self.name)))
167 } else {
168 None
169 }
170 }
171}
172
173impl<K, P> SourceCode for Diagnostics<'_, K, P>
174where
175 K: Title,
176 P: Send + Sync,
177{
178 fn read_span<'a>(
179 &'a self,
180 span: &SourceSpan,
181 context_lines_before: usize,
182 context_lines_after: usize,
183 ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
184 let inner = self
185 .text
186 .read_span(span, context_lines_before, context_lines_after)?;
187 let contents = MietteSpanContents::new_named(
188 self.name.to_string(),
189 inner.data(),
190 *inner.span(),
191 inner.line(),
192 inner.column(),
193 inner.line_count(),
194 )
195 .with_language("markdown");
196 Ok(Box::new(contents))
197 }
198}
199
200impl<K, P: IssueItem> fmt::Debug for Diagnostics<'_, K, P> {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 fmt::Debug::fmt(&self.status(), f)
203 }
204}
205
206impl<K, P: IssueItem> fmt::Display for Diagnostics<'_, K, P> {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 fmt::Display::fmt(&self.status().title(), f)
209 }
210}
211
212impl<K, P: IssueItem> std::error::Error for Diagnostics<'_, K, P> {}
213
214pub struct ReportBuilder<'a, K, P, F> {
216 items: Vec<Diagnostics<'a, K, P>>,
217 name_display: F,
218 level_filter: LevelFilter,
219}
220
221impl<'a, K, P, F> ReportBuilder<'a, K, P, F> {
222 pub fn new(items: Vec<Diagnostics<'a, K, P>>, name_display: F) -> Self {
223 Self {
224 items,
225 name_display,
226 level_filter: max_level(),
227 }
228 }
229
230 pub fn name_display<G>(self, name_display: G) -> ReportBuilder<'a, K, P, G>
232 where
233 G: for<'b> Fn(&'b K) -> String,
234 {
235 let Self {
236 items,
237 level_filter,
238 ..
239 } = self;
240 ReportBuilder {
241 items,
242 name_display,
243 level_filter,
244 }
245 }
246
247 pub fn level_filter(mut self, level: LevelFilter) -> Self {
248 self.level_filter = level;
249 self
250 }
251
252 pub fn filtered<Q>(mut self, mut f: impl FnMut(&K) -> bool) -> Self
253 where
254 K: Borrow<Q>,
255 Q: Eq + ?Sized,
256 {
257 self.items.retain(|d| f(&d.name));
258 self
259 }
260}
261
262impl<'a, K, P, F> ReportBuilder<'a, K, P, F>
263where
264 P: IssueItem,
265{
266 pub fn build(self) -> Reporter<'a, P>
267 where
268 F: for<'b> Fn(&'b K) -> String,
269 {
270 let Self {
271 items,
272 name_display,
273 level_filter,
274 } = self;
275
276 let items = items
277 .into_iter()
278 .flat_map(|Diagnostics { text, name, issues }| {
279 Self::grouped(level_filter, issues)
280 .into_iter()
281 .map(|(level, issues)| {
282 let name = name_display(&name);
283 (level, Diagnostics { text, name, issues })
284 })
285 .collect::<Vec<_>>()
286 })
287 .collect::<Vec<_>>()
288 .tap_mut(|items| {
289 items.sort_by(|(l1, d1), (l2, d2)| (l2, &d1.name).cmp(&(l1, &d2.name)))
290 })
291 .into_iter()
292 .map(|(_, d)| d)
293 .collect();
294
295 Reporter { items }
296 }
297
298 fn grouped(max: LevelFilter, issues: Vec<P>) -> BTreeMap<Level, Vec<P>> {
299 let mut groups = BTreeMap::<_, Vec<_>>::new();
300 for item in issues {
301 let level = item.issue().level();
302 if level > max {
303 continue;
304 }
305 groups.entry(level).or_default().push(item);
306 }
307 groups
308 }
309}
310
311pub struct Reporter<'a, P> {
312 items: Vec<Diagnostics<'a, String, P>>,
313}
314
315impl<P> Reporter<'_, P>
316where
317 P: IssueItem,
318{
319 pub fn to_level(&self) -> Option<Level> {
320 self.items.iter().map(|p| p.status().level()).min()
321 }
322
323 pub fn to_stderr(&self) -> &Self {
324 if self.items.is_empty() {
325 return self;
326 }
327
328 if is_logging() {
329 self.to_traces();
330 } else {
331 write!(stderr(), "\n{}", self.to_report())
332 .tap_err(emit_debug!())
333 .ok();
334 if let Some(level) = self.to_level() {
335 put_severity(level);
338 }
339 };
340
341 self
342 }
343
344 pub fn to_report(&self) -> String {
345 self.items.iter().fold(String::new(), |mut out, diag| {
346 writeln!(out, "{}", diag.to_report()).expect_fmt();
347 out
348 })
349 }
350
351 pub fn to_traces(&self) {
352 for item in self.items.iter() {
353 item.to_traces();
354 }
355 }
356}
357
358const fn level_style(level: Level) -> Style {
359 match level {
360 Level::TRACE => Style::new().dimmed(),
361 Level::DEBUG => Style::new().blue(),
362 Level::INFO => Style::new().green(),
363 Level::WARN => Style::new().yellow(),
364 Level::ERROR => Style::new().red(),
365 }
366}
367
368fn max_level() -> LevelFilter {
370 if tracing::enabled!(Level::TRACE) {
371 LevelFilter::TRACE
372 } else if tracing::enabled!(Level::DEBUG) {
373 LevelFilter::DEBUG
374 } else if tracing::enabled!(Level::INFO) {
375 LevelFilter::INFO
376 } else if tracing::enabled!(Level::WARN) {
377 LevelFilter::WARN
378 } else {
379 LevelFilter::ERROR
380 }
381}
382
383trait StyleCompat {
384 fn stderr(self) -> Self;
385}
386
387impl StyleCompat for Style {
388 fn stderr(self) -> Self {
389 if is_colored() { self } else { Style::new() }
390 }
391}
392
393pub trait Title: fmt::Display + Send + Sync {}
394
395impl<K: fmt::Display + Send + Sync> Title for K {}
396
397#[macro_export]
398macro_rules! plural {
399 ( $num:expr, $singular:expr ) => {
400 $crate::plural!($num, $singular, concat!($singular, "s"))
401 };
402 ( $num:expr, $singular:expr, $plural:expr ) => {{
403 let num = $num;
404 match num {
405 1 => format!("{num} {}", $singular),
406 _ => format!("{num} {}", $plural),
407 }
408 }};
409}