Skip to main content

prune_lang/cli/
diagnostic.rs

1use crate::syntax::lexer::Span;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct LineCol {
6    line: usize,
7    col: usize,
8}
9
10impl fmt::Display for LineCol {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        let LineCol { line, col } = self;
13        // index starts from 0 internally, but prints from 1
14        write!(f, "line {}, col {}", line + 1, col + 1)
15    }
16}
17
18fn get_line_col_vec(source: &str) -> Vec<LineCol> {
19    let mut vec = Vec::with_capacity(source.len());
20    let mut line = 0;
21    let mut col = 0;
22    for ch in source.chars() {
23        vec.push(LineCol { line, col });
24        if ch == '\n' {
25            line += 1;
26            col = 0;
27        } else {
28            col += 1;
29        }
30    }
31    vec
32}
33
34#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
35pub enum DiagLevel {
36    Error,
37    Warn,
38    Info,
39}
40
41impl fmt::Display for DiagLevel {
42    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43        match self {
44            DiagLevel::Error => write!(f, "Error"),
45            DiagLevel::Warn => write!(f, "Warn"),
46            DiagLevel::Info => write!(f, "Info"),
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Description {
53    verbosity: u8,
54    message: String,
55    span: Option<Span>,
56}
57
58impl Description {
59    pub fn message<S: Into<String>>(msg: S) -> Description {
60        Description {
61            verbosity: 10,
62            message: msg.into(),
63            span: None,
64        }
65    }
66
67    pub fn with_span(mut self, span: Span) -> Description {
68        self.span = Some(span);
69        self
70    }
71
72    pub fn with_verbosity(mut self, verbosity: u8) -> Description {
73        self.verbosity = verbosity;
74        self
75    }
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub struct Diagnostic {
80    pub level: DiagLevel,
81    title: String,
82    descriptions: Vec<Description>,
83}
84
85impl Diagnostic {
86    pub fn error<S: Into<String>>(title: S) -> Diagnostic {
87        Diagnostic {
88            level: DiagLevel::Error,
89            title: title.into(),
90            descriptions: Vec::new(),
91        }
92    }
93
94    pub fn warn<S: Into<String>>(title: S) -> Diagnostic {
95        Diagnostic {
96            level: DiagLevel::Warn,
97            title: title.into(),
98            descriptions: Vec::new(),
99        }
100    }
101
102    pub fn info<S: Into<String>>(title: S) -> Diagnostic {
103        Diagnostic {
104            level: DiagLevel::Info,
105            title: title.into(),
106            descriptions: Vec::new(),
107        }
108    }
109
110    pub fn line<S: Into<String>>(mut self, msg: S) -> Diagnostic {
111        self.descriptions.push(Description::message(msg));
112        self
113    }
114
115    pub fn line_span<S: Into<String>>(mut self, span: Span, msg: S) -> Diagnostic {
116        self.descriptions
117            .push(Description::message(msg).with_span(span));
118        self
119    }
120
121    pub fn line_verb<S: Into<String>>(mut self, verbosity: u8, msg: S) -> Diagnostic {
122        self.descriptions
123            .push(Description::message(msg).with_verbosity(verbosity));
124        self
125    }
126
127    pub fn line_span_verb<S: Into<String>>(
128        mut self,
129        span: Span,
130        verbosity: u8,
131        msg: S,
132    ) -> Diagnostic {
133        self.descriptions.push(
134            Description::message(msg)
135                .with_span(span)
136                .with_verbosity(verbosity),
137        );
138        self
139    }
140
141    /// minimal_report shows only spans, instead of source code.
142    pub fn minimal_report(&self, source: &str, verbosity: u8) -> String {
143        let mut output = format!("[{}]: {}\n", self.level, &self.title);
144        let linecol = get_line_col_vec(source);
145        for descr in &self.descriptions {
146            if descr.verbosity > verbosity {
147                // ignore those description with higher verbosity
148                continue;
149            }
150            match &descr.span {
151                Some(span) => {
152                    output.push_str(&format!(
153                        "{} - {}:\n{}\n",
154                        linecol[span.start], linecol[span.end], descr.message,
155                    ));
156                }
157                None => {
158                    output.push_str(&descr.message);
159                    output.push('\n');
160                }
161            }
162        }
163        output
164    }
165
166    pub fn report(&self, source: &str, verbosity: u8) -> String {
167        let mut output = format!("[{}]: {}\n", self.level, &self.title);
168        let linecol = get_line_col_vec(source);
169        for descr in &self.descriptions {
170            if descr.verbosity > verbosity {
171                // ignore those description with higher verbosity
172                continue;
173            }
174            match &descr.span {
175                Some(span) => {
176                    let start_line = linecol[span.start].line;
177                    let start_col = linecol[span.start].col;
178                    let end_line = linecol[span.end].line;
179                    let end_col = linecol[span.end].col;
180                    let text = source.lines().collect::<Vec<&str>>();
181                    let mut vec: Vec<(usize, usize)> = Vec::new();
182                    if start_line == end_line {
183                        vec.push((start_col, end_col))
184                    } else {
185                        for (idx, line) in
186                            text.iter().enumerate().take(end_line + 1).skip(start_line)
187                        {
188                            if idx == start_line {
189                                vec.push((start_col, line.chars().count()))
190                            } else if idx == end_line {
191                                vec.push((0, end_col))
192                            } else {
193                                vec.push((0, line.chars().count()))
194                            }
195                        }
196                    }
197
198                    let head_width = (1 + end_line).to_string().len();
199                    for (line, (s, e)) in (start_line..=end_line).zip(vec.into_iter()) {
200                        // print header "xxx | ", where xxx is the line number
201                        output.push_str(&format!("{:head_width$} | {}\n", line + 1, text[line]));
202                        output.push_str(&format!("{:head_width$} | ", ' '));
203                        for _ in 0..s {
204                            output.push(' ');
205                        }
206                        if line == start_line {
207                            output.push('^');
208                            for _ in s + 1..e {
209                                output.push('~');
210                            }
211                        } else if line == end_line {
212                            for _ in s..e {
213                                output.push('~');
214                            }
215                            output.push('^');
216                        } else {
217                            for _ in s..e {
218                                output.push('~');
219                            }
220                        }
221                        output.push('\n');
222                    }
223                    output.push_str(&descr.message);
224                    output.push('\n');
225                }
226                None => {
227                    output.push_str(&descr.message);
228                    output.push('\n');
229                }
230            }
231        }
232        output
233    }
234}
235
236#[test]
237fn diagnostic_test() {
238    let source = r#"1234567890
2391234567890
2401234567890
241"#;
242
243    let span = Span { start: 6, end: 25 };
244    let diag = Diagnostic::error("Error Name")
245        .line("some error description")
246        .line_span(span, "some spanned error description")
247        .line_verb(100, "this should not appear!");
248
249    assert_eq!(
250        diag.minimal_report(source, 30),
251        r#"[Error]: Error Name
252some error description
253line 1, col 7 - line 3, col 4:
254some spanned error description
255"#
256    );
257    assert_eq!(
258        diag.report(source, 30),
259        r#"[Error]: Error Name
260some error description
2611 | 1234567890
262  |       ^~~~
2632 | 1234567890
264  | ~~~~~~~~~~
2653 | 1234567890
266  | ~~~^
267some spanned error description
268"#
269    );
270}