Skip to main content

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 occurred 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 occurred.
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	/// Render the code, message, help text and labels of this diagnostic against `source`.
85	pub fn meta(&self, source: &str) -> DiagnosticMeta {
86		(self.formatter)(self, source)
87	}
88
89	/// The source range this diagnostic covers, from the first cursor to the last cursor consumed to recover.
90	pub fn span(&self) -> Span {
91		self.start_cursor.span() + self.end_cursor.span()
92	}
93
94	/// Get formatted message
95	pub fn message(&self, source: &str) -> String {
96		self.meta(source).message
97	}
98
99	/// Get diagnostic code
100	pub fn code(&self, source: &str) -> &'static str {
101		self.meta(source).code
102	}
103
104	/// Get help text
105	pub fn help(&self, source: &str) -> String {
106		self.meta(source).help
107	}
108
109	/// Add a desired cursor (what was expected)
110	pub fn with_desired_cursor(mut self, cursor: Cursor) -> Self {
111		self.desired_cursor = Some(cursor);
112		self
113	}
114
115	/// Convert to a full miette diagnostic for display
116	#[cfg(feature = "miette")]
117	pub fn into_diagnostic(self, source: &str) -> MietteDiagnostic {
118		use miette::LabeledSpan;
119		let DiagnosticMeta { code, message, help, mut labels } = self.meta(source);
120		let miette_labels = labels.drain(0..).map(|(span, label)| LabeledSpan::new_with_span(Some(label), span));
121		MietteDiagnostic::new(message)
122			.with_code(code)
123			.with_severity(self.severity.into())
124			.with_help(help)
125			.with_labels(miette_labels)
126	}
127
128	// Fomatting functions
129
130	pub fn unexpected(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
131		DiagnosticMeta {
132			code: "Unexpected",
133			message: format!("Unexpected `{:?}`", Kind::from(diagnostic.start_cursor)),
134			help: "This is not correct CSS syntax.".into(),
135			labels: vec![],
136		}
137	}
138
139	pub fn unexpected_ident(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
140		let cursor = diagnostic.start_cursor;
141		let start = cursor.offset().0 as usize;
142		let len = cursor.token().len() as usize;
143		let message = if start + len <= source.len() {
144			let text = &source[start..start + len];
145			format!("Unexpected identifier '{text}'")
146		} else {
147			"Unexpected identifier".to_string()
148		};
149		DiagnosticMeta {
150			code: "UnexpectedIdent",
151			message,
152			help: "There is an extra word which shouldn't be in this position.".into(),
153			labels: vec![],
154		}
155	}
156
157	pub fn unexpected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
158		let cursor = diagnostic.start_cursor;
159		let message = if let Some(char) = cursor.token().char() {
160			format!("Unexpected delimiter '{char}'")
161		} else {
162			"Unexpected delimiter".to_string()
163		};
164		DiagnosticMeta { code: "UnexpectedDelim", message, help: "Try removing the character.".into(), labels: vec![] }
165	}
166
167	pub fn expected_ident(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
168		DiagnosticMeta {
169			code: "ExpectedIdent",
170			message: format!("Expected an identifier but found `{:?}`", Kind::from(diagnostic.start_cursor)),
171			help: "This is not correct CSS syntax.".into(),
172			labels: vec![],
173		}
174	}
175
176	pub fn expected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
177		DiagnosticMeta {
178			code: "ExpectedDelim",
179			message: format!("Expected a delimiter but saw `{:?}`", Kind::from(diagnostic.start_cursor)),
180			help: "This is not correct CSS syntax.".into(),
181			labels: vec![],
182		}
183	}
184
185	pub fn bad_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
186		DiagnosticMeta {
187			code: "BadDeclaration",
188			message: "This declaration wasn't understood, and so was disregarded.".to_string(),
189			help: "The declaration contains invalid syntax, and will be ignored.".into(),
190			labels: vec![],
191		}
192	}
193
194	pub fn unknown_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
195		DiagnosticMeta {
196			code: "UnknownDeclaration",
197			message: "Ignored property due to parse error.".to_string(),
198			help: "This property is going to be ignored because it doesn't look valid. If it is valid, please file an issue!"
199				.into(),
200			labels: vec![],
201		}
202	}
203
204	pub fn expected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
205		DiagnosticMeta {
206			code: "ExpectedEnd",
207			message: "Expected this to be the end of the file, but there was more content.".to_string(),
208			help: "This is likely a problem with the parser. Please submit a bug report!".into(),
209			labels: vec![],
210		}
211	}
212
213	pub fn unexpected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
214		DiagnosticMeta {
215			code: "UnexpectedEnd",
216			message: "Expected more content but reached the end of the file.".to_string(),
217			help: "Perhaps this file isn't finished yet?".into(),
218			labels: vec![],
219		}
220	}
221
222	pub fn unexpected_close_curly(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
223		DiagnosticMeta {
224			code: "UnexpectedCloseCurly",
225			message: "Expected more content before this curly brace.".to_string(),
226			help: "This needed more content here".into(),
227			labels: vec![],
228		}
229	}
230
231	pub fn unexpected_tag(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
232		let cursor = diagnostic.start_cursor;
233		let start = cursor.offset().0 as usize;
234		let len = cursor.token().len() as usize;
235		let message = if start + len <= source.len() {
236			let text = &source[start..start + len];
237			format!("Unexpected tag name '{text}'")
238		} else {
239			"Unexpected tag name".to_string()
240		};
241		DiagnosticMeta { code: "UnexpectedTag", message, help: "This isn't a valid tag name.".into(), labels: vec![] }
242	}
243
244	pub fn unexpected_id(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
245		let cursor = diagnostic.start_cursor;
246		let start = cursor.offset().0 as usize;
247		let len = cursor.token().len() as usize;
248		let message = if start + len <= source.len() {
249			let text = &source[start..start + len];
250			format!("Unexpected ID selector '{text}'")
251		} else {
252			"Unexpected ID selector".to_string()
253		};
254		DiagnosticMeta { code: "UnexpectedId", message, help: "This isn't a valid ID.".into(), labels: vec![] }
255	}
256
257	pub fn opentype_tag_length(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
258		DiagnosticMeta {
259			code: "OpentypeTagLength",
260			message: "OpenType tag must be exactly 4 characters".to_string(),
261			help: "Feature and variation axis tags like 'kern', 'liga', 'wght' are always 4 ASCII characters.".into(),
262			labels: vec![],
263		}
264	}
265
266	pub fn opentype_tag_ascii(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
267		DiagnosticMeta {
268			code: "OpentypeTagAscii",
269			message: "OpenType tag contains invalid characters".to_string(),
270			help: "Tags must only contain ASCII characters in the range U+20-7E (printable ASCII).".into(),
271			labels: vec![],
272		}
273	}
274
275	pub fn invalid_unicode_range(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
276		DiagnosticMeta {
277			code: "InvalidUnicodeRange",
278			message: "Invalid unicode-range".to_string(),
279			help: "A unicode range looks like `U+0-7F`, `U+30??` or `U+4E00-9FFF`.".into(),
280			labels: vec![],
281		}
282	}
283}