biome_parser/
diagnostic.rs

1use crate::token_source::TokenSource;
2use crate::Parser;
3use biome_diagnostics::console::fmt::Display;
4use biome_diagnostics::console::{markup, MarkupBuf};
5use biome_diagnostics::location::AsSpan;
6use biome_diagnostics::{Advices, Diagnostic, Location, LogCategory, MessageAndDescription, Visit};
7use biome_rowan::{SyntaxKind, TextLen, TextRange};
8use std::cmp::Ordering;
9
10/// A specialized diagnostic for the parser
11///
12/// Parser diagnostics are always **errors**.
13///
14/// A parser diagnostics structured in this way:
15/// 1. a mandatory message and a mandatory [TextRange]
16/// 2. a list of details, useful to give more information and context around the error
17/// 3. a hint, which should tell the user how they could fix their issue
18///
19/// These information **are printed in this exact order**.
20///
21#[derive(Clone, Debug, Diagnostic)]
22#[diagnostic(category = "parse", severity = Error)]
23pub struct ParseDiagnostic {
24    /// The location where the error is occurred
25    #[location(span)]
26    span: Option<TextRange>,
27    #[message]
28    #[description]
29    pub message: MessageAndDescription,
30    #[advice]
31    advice: ParserAdvice,
32}
33
34/// Possible details related to the diagnostic
35#[derive(Clone, Debug, Default)]
36struct ParserAdvice {
37    advice_list: Vec<ParserAdviceKind>,
38}
39
40/// The structure of the advice. A message that gives details, a possible range so
41/// the diagnostic is able to highlight the part of the code we want to explain.
42#[derive(Clone, Debug)]
43struct ParserAdviceDetail {
44    /// A message that should explain this detail
45    message: MarkupBuf,
46    /// An optional range that should highlight the details of the code
47    span: Option<TextRange>,
48}
49
50#[derive(Clone, Debug)]
51enum ParserAdviceKind {
52    /// A list a possible details that can be attached to the diagnostic.
53    /// Useful to explain the nature errors.
54    Detail(ParserAdviceDetail),
55    /// A message for the user that should tell the user how to fix the issue
56    Hint(MarkupBuf),
57    List(MarkupBuf, Vec<MarkupBuf>),
58}
59
60impl ParserAdvice {
61    fn add_detail(&mut self, message: impl Display, range: impl AsSpan) {
62        self.advice_list
63            .push(ParserAdviceKind::Detail(ParserAdviceDetail {
64                message: markup! { {message} }.to_owned(),
65                span: range.as_span(),
66            }));
67    }
68
69    fn add_hint(&mut self, message: impl Display) {
70        self.advice_list
71            .push(ParserAdviceKind::Hint(markup! { { message } }.to_owned()));
72    }
73
74    fn add_hint_with_alternatives(&mut self, message: impl Display, alternatives: &[impl Display]) {
75        self.advice_list.push(ParserAdviceKind::List(
76            markup! {{message}}.to_owned(),
77            alternatives
78                .iter()
79                .map(|msg| markup! {{msg}}.to_owned())
80                .collect(),
81        ))
82    }
83}
84
85impl Advices for ParserAdvice {
86    fn record(&self, visitor: &mut dyn Visit) -> std::io::Result<()> {
87        for advice_kind in &self.advice_list {
88            match advice_kind {
89                ParserAdviceKind::Detail(detail) => {
90                    let ParserAdviceDetail { span, message } = detail;
91                    visitor.record_log(LogCategory::Info, message)?;
92
93                    let location = Location::builder().span(span).build();
94                    visitor.record_frame(location)?;
95                }
96                ParserAdviceKind::Hint(hint) => {
97                    visitor.record_log(LogCategory::Info, hint)?;
98                }
99                ParserAdviceKind::List(message, list) => {
100                    visitor.record_log(LogCategory::Info, message)?;
101
102                    let list: Vec<_> = list
103                        .iter()
104                        .map(|suggestion| suggestion as &dyn Display)
105                        .collect();
106                    visitor.record_list(&list)?;
107                }
108            }
109        }
110
111        Ok(())
112    }
113}
114
115impl ParseDiagnostic {
116    pub fn new(message: impl Display, span: impl AsSpan) -> Self {
117        Self {
118            span: span.as_span(),
119            message: MessageAndDescription::from(markup! { {message} }.to_owned()),
120            advice: ParserAdvice::default(),
121        }
122    }
123
124    pub fn new_single_node(name: &str, range: TextRange, p: &impl Parser) -> Self {
125        let names = format!("{} {}", article_for(name), name);
126        let msg = if p.source().text().text_len() <= range.start() {
127            format!("Expected {names} but instead found the end of the file.")
128        } else {
129            format!("Expected {} but instead found '{}'.", names, p.text(range))
130        };
131        Self {
132            span: range.as_span(),
133            message: MessageAndDescription::from(msg),
134            advice: ParserAdvice::default(),
135        }
136        .with_detail(range, format!("Expected {names} here."))
137    }
138
139    pub fn new_with_any(names: &[&str], range: TextRange, p: &impl Parser) -> Self {
140        debug_assert!(names.len() > 1, "Requires at least 2 names");
141
142        if names.len() < 2 {
143            return Self::new_single_node(names.first().unwrap_or(&"<missing>"), range, p);
144        }
145
146        let mut joined_names = String::new();
147
148        for (index, name) in names.iter().enumerate() {
149            if index > 0 {
150                joined_names.push_str(", ");
151            }
152
153            if index == names.len() - 1 {
154                joined_names.push_str("or ");
155            }
156
157            joined_names.push_str(article_for(name));
158            joined_names.push(' ');
159            joined_names.push_str(name);
160        }
161
162        let msg = if p.source().text().text_len() <= range.start() {
163            format!("Expected {joined_names} but instead found the end of the file.")
164        } else {
165            format!(
166                "Expected {} but instead found '{}'.",
167                joined_names,
168                p.text(range)
169            )
170        };
171
172        Self {
173            span: range.as_span(),
174            message: MessageAndDescription::from(msg),
175            advice: ParserAdvice::default(),
176        }
177        .with_detail(range, format!("Expected {joined_names} here."))
178    }
179
180    pub const fn is_error(&self) -> bool {
181        true
182    }
183
184    /// Use this API if you want to highlight more code frame, to help to explain where's the error.
185    ///
186    /// A detail is printed **after the actual error** and before the hint.
187    ///
188    /// ## Examples
189    ///
190    /// ```
191    /// # use biome_console::fmt::{Termcolor};
192    /// # use biome_console::markup;
193    /// # use biome_diagnostics::{DiagnosticExt, PrintDiagnostic, console::fmt::Formatter};
194    /// # use biome_parser::diagnostic::ParseDiagnostic;
195    /// # use biome_rowan::{TextSize, TextRange};
196    /// # use std::fmt::Write;
197    ///
198    /// let source = "const a";
199    /// let range = TextRange::new(TextSize::from(0), TextSize::from(5));
200    /// let mut diagnostic = ParseDiagnostic::new("this is wrong!", range)
201    ///     .with_detail(TextRange::new(TextSize::from(6), TextSize::from(7)), "This is reason why it's broken");
202    ///
203    /// let mut write = biome_diagnostics::termcolor::Buffer::no_color();
204    /// let error = diagnostic
205    ///     .clone()
206    ///     .with_file_path("example.js")
207    ///     .with_file_source_code(source.to_string());
208    /// Formatter::new(&mut Termcolor(&mut write))
209    ///     .write_markup(markup! {
210    ///     {PrintDiagnostic::verbose(&error)}
211    /// })
212    ///     .expect("failed to emit diagnostic");
213    ///
214    /// let mut result = String::new();
215    /// write!(
216    ///     result,
217    ///     "{}",
218    ///     std::str::from_utf8(write.as_slice()).expect("non utf8 in error buffer")
219    /// ).expect("");
220    pub fn with_detail(mut self, range: impl AsSpan, message: impl Display) -> Self {
221        self.advice.add_detail(message, range.as_span());
222        self
223    }
224
225    /// Small message that should suggest the user how they could fix the error
226    ///
227    /// Hints are rendered a **last part** of the diagnostics
228    ///
229    /// ## Examples
230    ///
231    /// ```
232    /// # use biome_console::fmt::{Termcolor};
233    /// # use biome_console::markup;
234    /// # use biome_diagnostics::{DiagnosticExt, PrintDiagnostic, console::fmt::Formatter};
235    /// # use biome_parser::diagnostic::ParseDiagnostic;
236    /// # use biome_rowan::{TextSize, TextRange};
237    /// # use std::fmt::Write;
238    ///
239    /// let source = "const a";
240    /// let range = TextRange::new(TextSize::from(0), TextSize::from(5));
241    /// let mut diagnostic = ParseDiagnostic::new("this is wrong!", range)
242    ///     .with_hint("You should delete the code");
243    ///
244    /// let mut write = biome_diagnostics::termcolor::Buffer::no_color();
245    /// let error = diagnostic
246    ///     .clone()
247    ///     .with_file_path("example.js")
248    ///     .with_file_source_code(source.to_string());
249    /// Formatter::new(&mut Termcolor(&mut write))
250    ///     .write_markup(markup! {
251    ///     {PrintDiagnostic::verbose(&error)}
252    /// })
253    ///     .expect("failed to emit diagnostic");
254    ///
255    /// let mut result = String::new();
256    /// write!(
257    ///     result,
258    ///     "{}",
259    ///     std::str::from_utf8(write.as_slice()).expect("non utf8 in error buffer")
260    /// ).expect("");
261    ///
262    /// assert!(result.contains("× this is wrong!"));
263    /// assert!(result.contains("i You should delete the code"));
264    /// assert!(result.contains("> 1 │ const a"));
265    /// ```
266    ///
267    pub fn with_hint(mut self, message: impl Display) -> Self {
268        self.advice.add_hint(message);
269        self
270    }
271
272    /// A message that also allows to list of alternatives in case a fixed range of values/characters are expected.
273    ///
274    /// ## Examples
275    ///
276    /// ```
277    /// # use biome_console::fmt::{Termcolor};
278    /// # use biome_console::markup;
279    /// # use biome_diagnostics::{DiagnosticExt, PrintDiagnostic, console::fmt::Formatter};
280    /// # use biome_parser::diagnostic::ParseDiagnostic;
281    /// # use biome_rowan::{TextSize, TextRange};
282    /// # use std::fmt::Write;
283    ///
284    /// let source = "const a";
285    /// let range = TextRange::new(TextSize::from(0), TextSize::from(5));
286    /// let mut diagnostic = ParseDiagnostic::new("this is wrong!", range)
287    ///     .with_alternatives("Expected one of the following values:", &["foo", "bar"]);
288    ///
289    /// let mut write = biome_diagnostics::termcolor::Buffer::no_color();
290    /// let error = diagnostic
291    ///     .clone()
292    ///     .with_file_path("example.js")
293    ///     .with_file_source_code(source.to_string());
294    /// Formatter::new(&mut Termcolor(&mut write))
295    ///     .write_markup(markup! {
296    ///     {PrintDiagnostic::verbose(&error)}
297    /// })
298    ///     .expect("failed to emit diagnostic");
299    ///
300    /// let mut result = String::new();
301    /// write!(
302    ///     result,
303    ///     "{}",
304    ///     std::str::from_utf8(write.as_slice()).expect("non utf8 in error buffer")
305    /// ).expect("");
306    ///
307    /// assert!(result.contains("× this is wrong!"));
308    /// assert!(result.contains("i Expected one of the following values:"));
309    /// assert!(result.contains("- foo"));
310    /// assert!(result.contains("- bar"));
311    /// ```
312    ///
313    pub fn with_alternatives(
314        mut self,
315        message: impl Display,
316        alternatives: &[impl Display],
317    ) -> Self {
318        self.advice
319            .add_hint_with_alternatives(message, alternatives);
320        self
321    }
322
323    /// Retrieves the range that belongs to the diagnostic
324    pub(crate) fn diagnostic_range(&self) -> Option<&TextRange> {
325        self.span.as_ref()
326    }
327}
328
329pub trait ToDiagnostic<P>
330where
331    P: Parser,
332{
333    fn into_diagnostic(self, p: &P) -> ParseDiagnostic;
334}
335
336impl<P: Parser> ToDiagnostic<P> for ParseDiagnostic {
337    fn into_diagnostic(self, _: &P) -> ParseDiagnostic {
338        self
339    }
340}
341
342#[must_use]
343pub fn expected_token<K>(token: K) -> ExpectedToken
344where
345    K: SyntaxKind,
346{
347    ExpectedToken(
348        token
349            .to_string()
350            .expect("Expected token to be a punctuation or keyword."),
351    )
352}
353
354#[must_use]
355pub fn expected_token_any<K: SyntaxKind>(tokens: &[K]) -> ExpectedTokens {
356    use std::fmt::Write;
357    let mut expected = String::new();
358
359    for (index, token) in tokens.iter().enumerate() {
360        if index > 0 {
361            expected.push_str(", ");
362        }
363
364        if index == tokens.len() - 1 {
365            expected.push_str("or ");
366        }
367
368        let _ = write!(
369            &mut expected,
370            "'{}'",
371            token
372                .to_string()
373                .expect("Expected token to be a punctuation or keyword.")
374        );
375    }
376
377    ExpectedTokens(expected)
378}
379
380pub struct ExpectedToken(&'static str);
381
382impl<P> ToDiagnostic<P> for ExpectedToken
383where
384    P: Parser,
385{
386    fn into_diagnostic(self, p: &P) -> ParseDiagnostic {
387        if p.cur() == P::Kind::EOF {
388            p.err_builder(
389                format!("expected `{}` but instead the file ends", self.0),
390                p.cur_range(),
391            )
392            .with_detail(p.cur_range(), "the file ends here")
393        } else {
394            p.err_builder(
395                format!("expected `{}` but instead found `{}`", self.0, p.cur_text()),
396                p.cur_range(),
397            )
398            .with_hint(format!("Remove {}", p.cur_text()))
399        }
400    }
401}
402
403pub struct ExpectedTokens(String);
404
405impl<P> ToDiagnostic<P> for ExpectedTokens
406where
407    P: Parser,
408{
409    fn into_diagnostic(self, p: &P) -> ParseDiagnostic {
410        if p.cur() == P::Kind::EOF {
411            p.err_builder(
412                format!("expected {} but instead the file ends", self.0),
413                p.cur_range(),
414            )
415            .with_detail(p.cur_range(), "the file ends here")
416        } else {
417            p.err_builder(
418                format!("expected {} but instead found `{}`", self.0, p.cur_text()),
419                p.cur_range(),
420            )
421            .with_hint(format!("Remove {}", p.cur_text()))
422        }
423    }
424}
425
426/// Creates a diagnostic saying that the node `name` was expected at range
427pub fn expected_node(name: &str, range: TextRange, p: &impl Parser) -> ParseDiagnostic {
428    ParseDiagnostic::new_single_node(name, range, p)
429}
430
431/// Creates a diagnostic saying that any of the nodes in `names` was expected at range
432pub fn expected_any(names: &[&str], range: TextRange, p: &impl Parser) -> ParseDiagnostic {
433    ParseDiagnostic::new_with_any(names, range, p)
434}
435
436/// Creates a diagnostic with message "Unexpected value." and then it lists the values that should be expected.
437pub fn expect_one_of(names: &[&str], range: TextRange) -> ParseDiagnostic {
438    ParseDiagnostic::new("Unexpected value or character.", range)
439        .with_alternatives("Expected one of:", names)
440}
441
442fn article_for(name: &str) -> &'static str {
443    match name.bytes().next() {
444        Some(b'a' | b'e' | b'i' | b'o' | b'u') => "an",
445        _ => "a",
446    }
447}
448
449/// Merges two lists of parser diagnostics. Only keeps the error from the first collection if two start at the same range.
450///
451/// The two lists must be so sorted by their source range in increasing order.
452pub fn merge_diagnostics(
453    first: Vec<ParseDiagnostic>,
454    second: Vec<ParseDiagnostic>,
455) -> Vec<ParseDiagnostic> {
456    if first.is_empty() {
457        return second;
458    }
459
460    if second.is_empty() {
461        return first;
462    }
463
464    let mut merged = Vec::new();
465
466    let mut first_iter = first.into_iter();
467    let mut second_iter = second.into_iter();
468
469    let mut current_first: Option<ParseDiagnostic> = first_iter.next();
470    let mut current_second: Option<ParseDiagnostic> = second_iter.next();
471
472    loop {
473        match (current_first, current_second) {
474            (Some(first_item), Some(second_item)) => {
475                let (first, second) = match (
476                    first_item.diagnostic_range(),
477                    second_item.diagnostic_range(),
478                ) {
479                    (Some(first_range), Some(second_range)) => {
480                        match first_range.start().cmp(&second_range.start()) {
481                            Ordering::Less => {
482                                merged.push(first_item);
483                                (first_iter.next(), Some(second_item))
484                            }
485                            Ordering::Equal => {
486                                // Only keep one error, skip the one from the second list.
487                                (Some(first_item), second_iter.next())
488                            }
489                            Ordering::Greater => {
490                                merged.push(second_item);
491                                (Some(first_item), second_iter.next())
492                            }
493                        }
494                    }
495                    (Some(_), None) => {
496                        merged.push(second_item);
497                        (Some(first_item), second_iter.next())
498                    }
499                    (None, Some(_)) => {
500                        merged.push(first_item);
501                        (first_iter.next(), Some(second_item))
502                    }
503                    (None, None) => {
504                        merged.push(first_item);
505                        merged.push(second_item);
506
507                        (first_iter.next(), second_iter.next())
508                    }
509                };
510
511                current_first = first;
512                current_second = second;
513            }
514
515            (None, None) => return merged,
516            (Some(first_item), None) => {
517                merged.push(first_item);
518                merged.extend(first_iter);
519                return merged;
520            }
521            (None, Some(second_item)) => {
522                merged.push(second_item);
523                merged.extend(second_iter);
524                return merged;
525            }
526        }
527    }
528}