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#[derive(Clone, Debug, Diagnostic)]
22#[diagnostic(category = "parse", severity = Error)]
23pub struct ParseDiagnostic {
24 #[location(span)]
26 span: Option<TextRange>,
27 #[message]
28 #[description]
29 pub message: MessageAndDescription,
30 #[advice]
31 advice: ParserAdvice,
32}
33
34#[derive(Clone, Debug, Default)]
36struct ParserAdvice {
37 advice_list: Vec<ParserAdviceKind>,
38}
39
40#[derive(Clone, Debug)]
43struct ParserAdviceDetail {
44 message: MarkupBuf,
46 span: Option<TextRange>,
48}
49
50#[derive(Clone, Debug)]
51enum ParserAdviceKind {
52 Detail(ParserAdviceDetail),
55 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 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 pub fn with_hint(mut self, message: impl Display) -> Self {
268 self.advice.add_hint(message);
269 self
270 }
271
272 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 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
426pub fn expected_node(name: &str, range: TextRange, p: &impl Parser) -> ParseDiagnostic {
428 ParseDiagnostic::new_single_node(name, range, p)
429}
430
431pub fn expected_any(names: &[&str], range: TextRange, p: &impl Parser) -> ParseDiagnostic {
433 ParseDiagnostic::new_with_any(names, range, p)
434}
435
436pub 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
449pub 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 (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}