Skip to main content

scarf_parser/
report.rs

1// =======================================================================
2// lib.rs
3// =======================================================================
4//! Reporting errors from parsing
5
6use crate::*;
7pub use ariadne::{Cache, ReportKind};
8
9/// A cache of sources that can be used for printing reports
10///
11/// Currently, this is implemented for iterators over
12/// `(file_name, file_content)` tuples; this should be the common use case,
13/// with others implemented at their own risk
14pub trait Sources {
15    fn sources(self) -> impl ariadne::Cache<String>;
16}
17
18impl<I, S> Sources for I
19where
20    I: IntoIterator<Item = (String, S)>,
21    S: AsRef<str>,
22{
23    fn sources(self) -> impl ariadne::Cache<String> {
24        ariadne::sources(self)
25    }
26}
27
28/// A printable error report
29///
30/// These include detailed information about the location of the error,
31/// and are printed with file snippets to assist the user
32pub struct Report {
33    builder: ariadne::ReportBuilder<'static, (String, std::ops::Range<usize>)>,
34}
35
36fn get_expansion_string(expansion_depth: usize, is_first: bool) -> String {
37    if expansion_depth == 0 {
38        "Original token".to_string()
39    } else if (expansion_depth == 1) && is_first {
40        "Expanded here".to_string()
41    } else {
42        let suffix = match expansion_depth % 10 {
43            1 => "st",
44            2 => "nd",
45            3 => "rd",
46            _ => "th",
47        };
48        format!("Expanded here {}{}", expansion_depth, suffix)
49    }
50}
51
52impl<'a> Report {
53    /// Create a new [`Report`] indicating a particular location with a message
54    ///
55    /// This does not label/print the location; to do so, use [`Report::with_label`]
56    pub fn new<C, M>(
57        kind: ariadne::ReportKind<'static>,
58        span: &Span<'a>,
59        code: C,
60        msg: M,
61    ) -> Self
62    where
63        C: std::fmt::Display,
64        M: ToString,
65    {
66        Self {
67            builder: ariadne::Report::build(
68                kind,
69                (span.file.to_string(), span.bytes.clone()),
70            )
71            .with_config(
72                ariadne::Config::new()
73                    .with_index_type(ariadne::IndexType::Byte),
74            )
75            .with_code(code)
76            .with_message(msg),
77        }
78    }
79
80    /// Adds a label to the [`Report`]
81    ///
82    /// A label includes a [`Span`] to highlight, as well as a message to
83    /// attach at that location
84    pub fn with_label<M>(
85        mut self,
86        span: &Span<'a>,
87        kind: ariadne::ReportKind<'static>,
88        msg: M,
89    ) -> Self
90    where
91        M: ToString,
92    {
93        let mut curr_span: &Span<'a> = span;
94        let mut expansion_depth: usize = curr_span.expansion_depth();
95        let mut expanded = false;
96        let color = match kind {
97            ReportKind::Error => Color::Red,
98            ReportKind::Warning => Color::Yellow,
99            ReportKind::Advice => Color::Fixed(147),
100            ReportKind::Custom(_, color) => color.clone(),
101        };
102        loop {
103            if let Some(expanded_span) = curr_span.expanded_from {
104                self.builder = self.builder.with_label(
105                    Label::new((
106                        curr_span.file.to_string(),
107                        curr_span.bytes.clone(),
108                    ))
109                    .with_message(get_expansion_string(
110                        expansion_depth,
111                        expanded == false,
112                    ))
113                    .with_color(Color::BrightGreen),
114                );
115                curr_span = expanded_span;
116                expansion_depth -= 1;
117                expanded = true;
118            } else {
119                break;
120            }
121        }
122        if expanded {
123            // Also label expansion in original spot
124            self.builder = self.builder.with_label(
125                Label::new((
126                    curr_span.file.to_string(),
127                    curr_span.bytes.clone(),
128                ))
129                .with_message(get_expansion_string(expansion_depth, false))
130                .with_color(Color::BrightGreen),
131            );
132        }
133        curr_span = &span;
134        self.builder = self.builder.with_label(
135            Label::new((curr_span.file.to_string(), curr_span.bytes.clone()))
136                .with_message(msg)
137                .with_color(color)
138                .with_priority(1),
139        );
140        let mut note = "".to_string();
141        let mut note_pad = "".to_string();
142        let total_inclusion_depth = curr_span.inclusion_depth();
143        let mut curr_inclusion_depth = 0;
144        // Only display a max of 7 includes
145        loop {
146            if (curr_inclusion_depth == 3) & (total_inclusion_depth > 7) {
147                for _ in 7..=total_inclusion_depth {
148                    curr_span = curr_span.included_from.unwrap();
149                }
150                note = format!("{}\n{}╰- ...", note, note_pad);
151                note_pad += "  ";
152            }
153            if let Some(included_span) = curr_span.included_from {
154                curr_inclusion_depth += 1;
155                curr_span = included_span;
156                if note.is_empty() {
157                    note = format!("Included from {}", curr_span.file);
158                } else {
159                    note = format!(
160                        "{}\n{}╰-Included from {}",
161                        note, note_pad, curr_span.file
162                    );
163                    note_pad += "  ";
164                }
165            } else {
166                break;
167            }
168        }
169        debug_assert!(curr_span.included_from.is_none());
170        if !note.is_empty() {
171            self.builder = self.builder.with_note(note);
172        }
173        self
174    }
175
176    /// Print the report to `stdout`
177    pub fn print<C>(self, source_cache: &mut C) -> std::io::Result<()>
178    where
179        C: ariadne::Cache<String>,
180    {
181        let report = self.builder.finish();
182        report.print(source_cache)
183    }
184
185    /// Print the report to `stderr`
186    pub fn eprint<C>(self, source_cache: &mut C) -> std::io::Result<()>
187    where
188        C: ariadne::Cache<String>,
189    {
190        let report = self.builder.finish();
191        report.eprint(source_cache)
192    }
193
194    /// Writes the report (without color) to a target with the [`std::io::Write`] trait
195    pub fn write<C, W>(
196        self,
197        source_cache: &mut C,
198        target: W,
199    ) -> std::io::Result<()>
200    where
201        C: ariadne::Cache<String>,
202        W: std::io::Write,
203    {
204        let report = self.builder.finish();
205        report.write(source_cache, target)
206    }
207}