css_parse/
diagnostics.rs

1use crate::{Cursor, Kind};
2use css_lexer::Span;
3#[cfg(feature = "miette")]
4use miette::{MietteDiagnostic, Severity as MietteSeverity};
5use std::fmt::{Display, Formatter, Result};
6
7type DiagnosticFormatter = fn(&Diagnostic, &str) -> DiagnosticMeta;
8
9/// An issue that occured during parse time.
10#[repr(C, align(64))]
11#[derive(Debug, Copy, Clone)]
12pub struct Diagnostic {
13	/// How severe this error is.
14	pub severity: Severity,
15	/// The first cursor where this error occured.
16	pub start_cursor: Cursor,
17	/// The last cursor that was consumed to recover from this error.
18	pub end_cursor: Cursor,
19	/// A cursor representing what was expected.
20	pub desired_cursor: Option<Cursor>,
21	/// Function pointer to format the message template with cursor/span data
22	pub formatter: DiagnosticFormatter,
23}
24
25pub struct DiagnosticMeta {
26	pub code: &'static str,
27	pub message: String,
28	pub help: String,
29	pub labels: Vec<(Span, String)>,
30}
31
32#[derive(Debug, Clone, Copy)]
33pub enum Severity {
34	Advice,
35	Warning,
36	Error,
37}
38
39impl Severity {
40	pub const fn as_str(&self) -> &str {
41		match *self {
42			Self::Advice => "Advice",
43			Self::Warning => "Warning",
44			Self::Error => "Error",
45		}
46	}
47}
48
49impl Display for Severity {
50	fn fmt(&self, f: &mut Formatter<'_>) -> Result {
51		write!(f, "{}", self.as_str())
52	}
53}
54
55#[cfg(feature = "miette")]
56impl From<Severity> for MietteSeverity {
57	fn from(value: Severity) -> Self {
58		match value {
59			Severity::Advice => MietteSeverity::Advice,
60			Severity::Warning => MietteSeverity::Warning,
61			Severity::Error => MietteSeverity::Error,
62		}
63	}
64}
65
66impl Diagnostic {
67	/// Create a new diagnostic
68	pub fn new(start_cursor: Cursor, formatter: DiagnosticFormatter) -> Self {
69		Self { severity: Severity::Error, start_cursor, end_cursor: start_cursor, desired_cursor: None, formatter }
70	}
71
72	/// Apply a severity to the given Diagnostic.
73	pub fn with_severity(mut self, severity: Severity) -> Self {
74		self.severity = severity;
75		self
76	}
77
78	/// Apply an end Cursor to the given Diagnostic.
79	pub fn with_end_cursor(mut self, end_cursor: Cursor) -> Self {
80		self.end_cursor = end_cursor;
81		self
82	}
83
84	/// Get formatted message
85	pub fn message(&self, source: &str) -> String {
86		let DiagnosticMeta { message, .. } = (self.formatter)(self, source);
87		message
88	}
89
90	/// Get diagnostic code
91	pub fn code(&self, source: &str) -> &'static str {
92		let DiagnosticMeta { code, .. } = (self.formatter)(self, source);
93		code
94	}
95
96	/// Get help text
97	pub fn help(&self, source: &str) -> String {
98		let DiagnosticMeta { help, .. } = (self.formatter)(self, source);
99		help
100	}
101
102	/// Add a desired cursor (what was expected)
103	pub fn with_desired_cursor(mut self, cursor: Cursor) -> Self {
104		self.desired_cursor = Some(cursor);
105		self
106	}
107
108	/// Convert to a full miette diagnostic for display
109	#[cfg(feature = "miette")]
110	pub fn into_diagnostic(self, source: &str) -> MietteDiagnostic {
111		use miette::LabeledSpan;
112		let DiagnosticMeta { code, message, help, mut labels } = (self.formatter)(&self, source);
113		let miette_labels = labels.drain(0..).map(|(span, label)| LabeledSpan::new_with_span(Some(label), span));
114		MietteDiagnostic::new(message)
115			.with_code(code)
116			.with_severity(self.severity.into())
117			.with_help(help)
118			.with_labels(miette_labels)
119	}
120
121	// Fomatting functions
122
123	pub fn unexpected(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
124		DiagnosticMeta {
125			code: "Unexpected",
126			message: format!("Unexpected `{:?}`", Kind::from(diagnostic.start_cursor)),
127			help: "This is not correct CSS syntax.".into(),
128			labels: vec![],
129		}
130	}
131
132	pub fn unexpected_ident(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
133		let cursor = diagnostic.start_cursor;
134		let start = cursor.offset().0 as usize;
135		let len = cursor.token().len() as usize;
136		let message = if start + len <= source.len() {
137			let text = &source[start..start + len];
138			format!("Unexpected identifier '{text}'")
139		} else {
140			"Unexpected identifier".to_string()
141		};
142		DiagnosticMeta {
143			code: "UnexpectedIdent",
144			message,
145			help: "There is an extra word which shouldn't be in this position.".into(),
146			labels: vec![],
147		}
148	}
149
150	pub fn unexpected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
151		let cursor = diagnostic.start_cursor;
152		let message = if let Some(char) = cursor.token().char() {
153			format!("Unexpected delimiter '{char}'")
154		} else {
155			"Unexpected delimiter".to_string()
156		};
157		DiagnosticMeta { code: "UnexpectedDelim", message, help: "Try removing the character.".into(), labels: vec![] }
158	}
159
160	pub fn expected_ident(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
161		DiagnosticMeta {
162			code: "ExpectedIdent",
163			message: format!("Expected an identifier but found `{:?}`", Kind::from(diagnostic.start_cursor)),
164			help: "This is not correct CSS syntax.".into(),
165			labels: vec![],
166		}
167	}
168
169	pub fn expected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
170		DiagnosticMeta {
171			code: "ExpectedDelim",
172			message: format!("Expected a delimiter but saw `{:?}`", Kind::from(diagnostic.start_cursor)),
173			help: "This is not correct CSS syntax.".into(),
174			labels: vec![],
175		}
176	}
177
178	pub fn bad_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
179		DiagnosticMeta {
180			code: "BadDeclaration",
181			message: "This declaration wasn't understood, and so was disregarded.".to_string(),
182			help: "The declaration contains invalid syntax, and will be ignored.".into(),
183			labels: vec![],
184		}
185	}
186
187	pub fn unknown_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
188		DiagnosticMeta {
189			code: "UnknownDeclaration",
190			message: "Ignored property due to parse error.".to_string(),
191			help: "This property is going to be ignored because it doesn't look valid. If it is valid, please file an issue!"
192				.into(),
193			labels: vec![],
194		}
195	}
196
197	pub fn expected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
198		DiagnosticMeta {
199			code: "ExpectedEnd",
200			message: "Expected this to be the end of the file, but there was more content.".to_string(),
201			help: "This is likely a problem with the parser. Please submit a bug report!".into(),
202			labels: vec![],
203		}
204	}
205
206	pub fn unexpected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
207		DiagnosticMeta {
208			code: "UnexpectedEnd",
209			message: "Expected more content but reached the end of the file.".to_string(),
210			help: "Perhaps this file isn't finished yet?".into(),
211			labels: vec![],
212		}
213	}
214
215	pub fn unexpected_close_curly(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
216		DiagnosticMeta {
217			code: "UnexpectedCloseCurly",
218			message: "Expected more content before this curly brace.".to_string(),
219			help: "This needed more content here".into(),
220			labels: vec![],
221		}
222	}
223
224	pub fn unexpected_tag(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
225		let cursor = diagnostic.start_cursor;
226		let start = cursor.offset().0 as usize;
227		let len = cursor.token().len() as usize;
228		let message = if start + len <= source.len() {
229			let text = &source[start..start + len];
230			format!("Unexpected tag name '{text}'")
231		} else {
232			"Unexpected tag name".to_string()
233		};
234		DiagnosticMeta { code: "UnexpectedTag", message, help: "This isn't a valid tag name.".into(), labels: vec![] }
235	}
236
237	pub fn unexpected_id(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
238		let cursor = diagnostic.start_cursor;
239		let start = cursor.offset().0 as usize;
240		let len = cursor.token().len() as usize;
241		let message = if start + len <= source.len() {
242			let text = &source[start..start + len];
243			format!("Unexpected ID selector '{text}'")
244		} else {
245			"Unexpected ID selector".to_string()
246		};
247		DiagnosticMeta { code: "UnexpectedId", message, help: "This isn't a valid ID.".into(), labels: vec![] }
248	}
249}