aldrin_parser/
diag.rs

1//! Diagnostic information and formatting.
2//!
3//! This module primarily provides the [`Diagnostic`] trait, which is implemented by all
4//! [errors](crate::error) and [warnings](crate::warning).
5
6use crate::{Parsed, Position, Schema, Span};
7use std::borrow::Cow;
8use std::fmt;
9use std::path::Path;
10use unicode_segmentation::UnicodeSegmentation;
11use unicode_width::UnicodeWidthStr;
12
13/// Diagnostic information about an error or a warning.
14pub trait Diagnostic {
15    /// Kind of the diagnostic; either an error or a warning.
16    fn kind(&self) -> DiagnosticKind;
17
18    /// Name of the schema this diagnostic originated from.
19    ///
20    /// The schema name can be used to look up the [`Schema`] with [`Parsed::get_schema`].
21    fn schema_name(&self) -> &str;
22
23    /// Formats the diagnostic for printing.
24    fn format<'a>(&'a self, parsed: &'a Parsed) -> Formatted<'a>;
25}
26
27/// Error or warning.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub enum DiagnosticKind {
30    /// Indicates an issue which prevents further processing.
31    Error,
32
33    /// Indicates an issue which doesn't prevent further processing.
34    Warning,
35}
36
37/// Style of chunk in a formatted diagnostic.
38///
39/// This describes broadly what particular chunk in [`Formatted`] contains and how it should be
40/// styled.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum Style {
43    /// Regular / unstyled output.
44    Regular,
45
46    /// Output e.g. as red and bold.
47    Error,
48
49    /// Output e.g. as yellow and bold.
50    Warning,
51
52    /// Output e.g. as blue and bold.
53    Info,
54
55    /// Output e.g. as bold.
56    Emphasized,
57
58    /// Output e.g. green.
59    Separator,
60
61    /// Output e.g. green and bold.
62    LineNumber,
63}
64
65/// A diagnostic formatted for printing.
66///
67/// A `Formatted` can be printing and converted to a string directly via it's
68/// [`Display`](fmt::Display) implementation.
69///
70/// Alternatively, you can iterate over the individual [`Line`s](Line) and from there over pairs of
71/// `&str` and [`Style`]. This allows you to apply custom styling (e.g. colors) to individual parts
72/// of the diagnostic.
73///
74/// `Formatted` also provides a short one-line [`summary`](Formatted::summary).
75///
76/// # Example
77///
78/// ```
79/// use aldrin_parser::{Diagnostic, Parser};
80///
81/// let parsed = Parser::new().parse("schemas/duplicate_id.aldrin");
82/// let err = &parsed.errors()[0];
83/// let formatted = err.format(&parsed);
84///
85/// // Print via Display:
86/// eprintln!("{}", formatted);
87///
88/// // Print a one-line summary:
89/// eprintln!("Error: {}.", formatted.summary());
90///
91/// // Print manually to apply styling to the output:
92/// for line in &formatted {
93///     for (chunk, style) in line {
94///         // Apply style to chunk, e.g. colorize the output.
95///         eprint!("{}", chunk);
96///     }
97///
98///     eprintln!();
99/// }
100/// ```
101#[derive(Debug, Clone)]
102pub struct Formatted<'a> {
103    kind: DiagnosticKind,
104    intro: Line<'a>,
105    lines: Vec<Line<'a>>,
106}
107
108impl<'a> Formatted<'a> {
109    /// Kind of the diagnostic; error or warning.
110    pub fn kind(&self) -> DiagnosticKind {
111        self.kind
112    }
113
114    /// Short one-line summary.
115    ///
116    /// The summary begins with a lower-case letter and doesn't end with any punctuation.
117    ///
118    /// # Example
119    ///
120    /// ```
121    /// # use aldrin_parser::{Diagnostic, Parser};
122    /// # let parsed = Parser::new().parse("schemas/duplicate_id.aldrin");
123    /// # let err = &parsed.errors()[0];
124    /// # let formatted = err.format(&parsed);
125    /// eprintln!("An issue occurred: {}.", formatted.summary());
126    /// ```
127    pub fn summary(&self) -> &str {
128        &self.intro.chunks[2].0
129    }
130
131    /// Returns the number of lines.
132    #[allow(clippy::len_without_is_empty)]
133    pub fn len(&self) -> usize {
134        self.lines.len() + 1
135    }
136
137    /// Returns an iterator over the lines.
138    pub fn lines(&'a self) -> Lines<'a> {
139        Lines {
140            intro: &self.intro,
141            lines: &self.lines,
142            line: 0,
143        }
144    }
145}
146
147impl<'a> IntoIterator for &'a Formatted<'a> {
148    type Item = &'a Line<'a>;
149    type IntoIter = Lines<'a>;
150
151    fn into_iter(self) -> Self::IntoIter {
152        self.lines()
153    }
154}
155
156impl fmt::Display for Formatted<'_> {
157    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158        for line in self {
159            line.fmt(f)?;
160        }
161
162        Ok(())
163    }
164}
165
166/// Iterator over the lines of a formatted diagnostic.
167#[derive(Debug)]
168pub struct Lines<'a> {
169    intro: &'a Line<'a>,
170    lines: &'a [Line<'a>],
171    line: usize,
172}
173
174impl<'a> Iterator for Lines<'a> {
175    type Item = &'a Line<'a>;
176
177    fn next(&mut self) -> Option<Self::Item> {
178        if self.line == 0 {
179            self.line += 1;
180            Some(self.intro)
181        } else if self.line <= self.lines.len() {
182            let line = &self.lines[self.line - 1];
183            self.line += 1;
184            Some(line)
185        } else {
186            None
187        }
188    }
189}
190
191/// Line of a formatted diagnostic.
192#[derive(Debug, Clone)]
193pub struct Line<'a> {
194    padding: Cow<'a, str>,
195    chunks: Vec<(Cow<'a, str>, Style)>,
196}
197
198impl<'a> Line<'a> {
199    /// Returns the number of chunks in this line.
200    #[allow(clippy::len_without_is_empty)]
201    pub fn len(&self) -> usize {
202        self.chunks.len() + 1
203    }
204
205    /// Returns an iterator of chunks in this line.
206    pub fn chunks(&'a self) -> Chunks<'a> {
207        Chunks {
208            line: self,
209            chunk: 0,
210        }
211    }
212}
213
214impl<'a> IntoIterator for &'a Line<'a> {
215    type Item = (&'a str, Style);
216    type IntoIter = Chunks<'a>;
217
218    fn into_iter(self) -> Self::IntoIter {
219        self.chunks()
220    }
221}
222
223impl fmt::Display for Line<'_> {
224    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225        for (chunk, _) in self {
226            f.write_str(chunk)?;
227        }
228
229        writeln!(f)
230    }
231}
232
233/// Iterator of chunks in a line.
234#[derive(Debug, Clone)]
235pub struct Chunks<'a> {
236    line: &'a Line<'a>,
237    chunk: usize,
238}
239
240impl<'a> Iterator for Chunks<'a> {
241    type Item = (&'a str, Style);
242
243    fn next(&mut self) -> Option<Self::Item> {
244        if self.chunk == 0 {
245            self.chunk += 1;
246            Some((&self.line.padding, Style::Regular))
247        } else if self.chunk <= self.line.chunks.len() {
248            let chunk = &self.line.chunks[self.chunk - 1];
249            self.chunk += 1;
250            Some((&chunk.0, chunk.1))
251        } else {
252            None
253        }
254    }
255}
256
257pub(crate) struct Formatter<'a> {
258    kind: DiagnosticKind,
259    intro: Line<'a>,
260    lines: Vec<UnpaddedLine<'a>>,
261    padding: usize,
262}
263
264impl<'a> Formatter<'a> {
265    pub fn new<D, S>(diagnostic: &'a D, summary: S) -> Self
266    where
267        D: Diagnostic,
268        S: Into<Cow<'a, str>>,
269    {
270        let (kind, kind_style) = match diagnostic.kind() {
271            DiagnosticKind::Error => ("error", Style::Error),
272            DiagnosticKind::Warning => ("warning", Style::Warning),
273        };
274        let summary = summary.into();
275        let sep = if summary.is_empty() { ":" } else { ": " };
276
277        let intro = Line {
278            padding: "".into(),
279            chunks: vec![
280                (kind.into(), kind_style),
281                (sep.into(), Style::Emphasized),
282                // Formatted::summary() requires this (and only this) to be the 3rd entry.
283                (summary, Style::Emphasized),
284            ],
285        };
286
287        Formatter {
288            kind: diagnostic.kind(),
289            intro,
290            lines: Vec::new(),
291            padding: 0,
292        }
293    }
294
295    pub fn format(self) -> Formatted<'a> {
296        let mut lines = Vec::with_capacity(self.lines.len());
297        for line in self.lines {
298            lines.push(Line {
299                padding: gen_padding(self.padding - line.padding),
300                chunks: line.chunks,
301            });
302        }
303
304        Formatted {
305            kind: self.kind,
306            intro: self.intro,
307            lines,
308        }
309    }
310
311    pub fn block<S>(
312        &mut self,
313        schema: &'a Schema,
314        location: Position,
315        indicator: Span,
316        text: S,
317        is_main_block: bool,
318    ) -> &mut Self
319    where
320        S: Into<Cow<'a, str>>,
321    {
322        self.location(schema.path(), location, is_main_block);
323
324        let source = match schema.source() {
325            Some(source) => source,
326            None => return self,
327        };
328
329        #[derive(PartialEq, Eq)]
330        enum State {
331            Normal,
332            Skip,
333        }
334
335        self.empty_context();
336        let mut state = State::Normal;
337        let mut lines = indicator.lines(source).peekable();
338
339        while let Some((line, span)) = lines.next() {
340            let line = line.trim_end();
341
342            if line.trim().is_empty() {
343                if state == State::Skip {
344                    continue;
345                }
346
347                if let Some((next, _)) = lines.peek() {
348                    if next.trim().is_empty() {
349                        state = State::Skip;
350                        self.skipped_context();
351                        self.empty_context();
352                        continue;
353                    }
354                } else {
355                    continue;
356                }
357            }
358
359            state = State::Normal;
360
361            let trimmed = line.trim_start();
362            let diff = line.len() - trimmed.len();
363            let (source, from) = if diff >= 8 {
364                self.trimmed_context(span.from.line_col.line, trimmed);
365                let from = span.from.line_col.column.saturating_sub(diff + 1);
366                let to = span.to.line_col.column - diff - 1;
367                let trimmed = &trimmed[from..to];
368                (trimmed, from + 4)
369            } else {
370                self.context(span.from.line_col.line, line);
371                let from = span.from.line_col.column - 1;
372                let to = span.to.line_col.column - 1;
373                (&line[from..to], from)
374            };
375
376            let to: usize = source.graphemes(true).map(UnicodeWidthStr::width).sum();
377
378            if lines.peek().is_some() {
379                self.indicator(from, from + to, "", is_main_block);
380            } else {
381                self.indicator(from, from + to, text, is_main_block);
382                self.empty_context();
383                break;
384            }
385        }
386
387        self
388    }
389
390    pub fn main_block<S>(
391        &mut self,
392        schema: &'a Schema,
393        location: Position,
394        span: Span,
395        text: S,
396    ) -> &mut Self
397    where
398        S: Into<Cow<'a, str>>,
399    {
400        self.block(schema, location, span, text, true)
401    }
402
403    pub fn info_block<S>(
404        &mut self,
405        schema: &'a Schema,
406        location: Position,
407        span: Span,
408        text: S,
409    ) -> &mut Self
410    where
411        S: Into<Cow<'a, str>>,
412    {
413        self.block(schema, location, span, text, false)
414    }
415
416    pub fn location<P>(&mut self, path: P, location: Position, is_main_location: bool) -> &mut Self
417    where
418        P: AsRef<Path>,
419    {
420        if is_main_location {
421            self.main_location(path, location)
422        } else {
423            self.info_location(path, location)
424        }
425    }
426
427    pub fn main_location<P>(&mut self, path: P, location: Position) -> &mut Self
428    where
429        P: AsRef<Path>,
430    {
431        self.location_impl(path, location, "-->")
432    }
433
434    pub fn info_location<P>(&mut self, path: P, location: Position) -> &mut Self
435    where
436        P: AsRef<Path>,
437    {
438        self.location_impl(path, location, ":::")
439    }
440
441    fn location_impl<P, S>(&mut self, path: P, location: Position, sep: S) -> &mut Self
442    where
443        P: AsRef<Path>,
444        S: Into<Cow<'a, str>>,
445    {
446        let location = format!(
447            " {}:{}:{}",
448            path.as_ref().display(),
449            location.line_col.line,
450            location.line_col.column
451        );
452
453        self.lines.push(UnpaddedLine::new(vec![
454            (" ".into(), Style::Regular),
455            (sep.into(), Style::Separator),
456            (location.into(), Style::Regular),
457        ]));
458
459        self
460    }
461
462    pub fn empty_context(&mut self) -> &mut Self {
463        self.lines.push(UnpaddedLine::new(vec![
464            ("  ".into(), Style::Regular),
465            ("|".into(), Style::Separator),
466        ]));
467
468        self
469    }
470
471    pub fn context<S>(&mut self, line: usize, source: S) -> &mut Self
472    where
473        S: Into<Cow<'a, str>>,
474    {
475        let line = line.to_string();
476        if line.len() > self.padding {
477            self.padding = line.len();
478        }
479
480        self.lines.push(UnpaddedLine::with_padding(
481            line.len(),
482            vec![
483                (" ".into(), Style::Regular),
484                (line.into(), Style::LineNumber),
485                (" ".into(), Style::Regular),
486                ("|".into(), Style::Separator),
487                (" ".into(), Style::Regular),
488                (source.into(), Style::Regular),
489            ],
490        ));
491
492        self
493    }
494
495    pub fn trimmed_context<S>(&mut self, line: usize, source: S) -> &mut Self
496    where
497        S: Into<Cow<'a, str>>,
498    {
499        let line = line.to_string();
500        if line.len() > self.padding {
501            self.padding = line.len();
502        }
503
504        self.lines.push(UnpaddedLine::with_padding(
505            line.len(),
506            vec![
507                (" ".into(), Style::Regular),
508                (line.into(), Style::LineNumber),
509                (" ".into(), Style::Regular),
510                ("|".into(), Style::Separator),
511                (" ... ".into(), Style::Regular),
512                (source.into(), Style::Regular),
513            ],
514        ));
515
516        self
517    }
518
519    pub fn skipped_context(&mut self) -> &mut Self {
520        let skip = "..";
521        if skip.len() > self.padding {
522            self.padding = skip.len();
523        }
524
525        self.lines.push(UnpaddedLine::with_padding(
526            skip.len(),
527            vec![
528                (" ".into(), Style::Regular),
529                (skip.into(), Style::LineNumber),
530                (" ".into(), Style::Regular),
531                ("|".into(), Style::Separator),
532            ],
533        ));
534
535        self
536    }
537
538    pub fn indicator<S>(
539        &mut self,
540        from: usize,
541        to: usize,
542        text: S,
543        is_main_indicator: bool,
544    ) -> &mut Self
545    where
546        S: Into<Cow<'a, str>>,
547    {
548        if is_main_indicator {
549            self.main_indicator(from, to, text)
550        } else {
551            self.info_indicator(from, to, text)
552        }
553    }
554
555    pub fn main_indicator<S>(&mut self, from: usize, to: usize, text: S) -> &mut Self
556    where
557        S: Into<Cow<'a, str>>,
558    {
559        let style = match self.kind {
560            DiagnosticKind::Error => Style::Error,
561            DiagnosticKind::Warning => Style::Warning,
562        };
563
564        self.indicator_impl(from, text, gen_main_indicator(to - from), style)
565    }
566
567    pub fn info_indicator<S>(&mut self, from: usize, to: usize, text: S) -> &mut Self
568    where
569        S: Into<Cow<'a, str>>,
570    {
571        self.indicator_impl(from, text, gen_info_indicator(to - from), Style::Info)
572    }
573
574    fn indicator_impl<S, I>(&mut self, from: usize, text: S, ind: I, style: Style) -> &mut Self
575    where
576        S: Into<Cow<'a, str>>,
577        I: Into<Cow<'a, str>>,
578    {
579        let mut line = UnpaddedLine::new(vec![
580            ("  ".into(), Style::Regular),
581            ("|".into(), Style::Separator),
582            (gen_padding(from + 1), Style::Regular),
583            (ind.into(), style),
584        ]);
585
586        let text = text.into();
587        if !text.is_empty() {
588            line.chunks.push((" ".into(), Style::Regular));
589            line.chunks.push((text, style));
590        }
591
592        self.lines.push(line);
593        self
594    }
595
596    pub fn note<S>(&mut self, text: S) -> &mut Self
597    where
598        S: Into<Cow<'a, str>>,
599    {
600        self.info_impl("note", text)
601    }
602
603    pub fn help<S>(&mut self, text: S) -> &mut Self
604    where
605        S: Into<Cow<'a, str>>,
606    {
607        self.info_impl("help", text)
608    }
609
610    fn info_impl<K, S>(&mut self, kind: K, text: S) -> &mut Self
611    where
612        K: Into<Cow<'a, str>>,
613        S: Into<Cow<'a, str>>,
614    {
615        self.lines.push(UnpaddedLine::new(vec![
616            ("  ".into(), Style::Regular),
617            ("=".into(), Style::Separator),
618            (" ".into(), Style::Regular),
619            (kind.into(), Style::Emphasized),
620            (":".into(), Style::Emphasized),
621            (" ".into(), Style::Regular),
622            (text.into(), Style::Regular),
623        ]));
624
625        self
626    }
627}
628
629struct UnpaddedLine<'a> {
630    padding: usize,
631    chunks: Vec<(Cow<'a, str>, Style)>,
632}
633
634impl<'a> UnpaddedLine<'a> {
635    fn new(chunks: Vec<(Cow<'a, str>, Style)>) -> Self {
636        UnpaddedLine { padding: 0, chunks }
637    }
638
639    fn with_padding(padding: usize, chunks: Vec<(Cow<'a, str>, Style)>) -> Self {
640        UnpaddedLine { padding, chunks }
641    }
642}
643
644fn gen_padding(size: usize) -> Cow<'static, str> {
645    const PADDING: &str = "                                                                ";
646    if size < PADDING.len() {
647        PADDING[..size].into()
648    } else {
649        " ".repeat(size).into()
650    }
651}
652
653fn gen_main_indicator(size: usize) -> Cow<'static, str> {
654    const INDICATOR: &str = "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^";
655    if size < INDICATOR.len() {
656        INDICATOR[..size].into()
657    } else {
658        "^".repeat(size).into()
659    }
660}
661
662fn gen_info_indicator(size: usize) -> Cow<'static, str> {
663    const INDICATOR: &str = "----------------------------------------------------------------";
664    if size < INDICATOR.len() {
665        INDICATOR[..size].into()
666    } else {
667        "-".repeat(size).into()
668    }
669}