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#[repr(C, align(64))]
11#[derive(Debug, Copy, Clone)]
12pub struct Diagnostic {
13 pub severity: Severity,
15 pub start_cursor: Cursor,
17 pub end_cursor: Cursor,
19 pub desired_cursor: Option<Cursor>,
21 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 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 pub fn with_severity(mut self, severity: Severity) -> Self {
74 self.severity = severity;
75 self
76 }
77
78 pub fn with_end_cursor(mut self, end_cursor: Cursor) -> Self {
80 self.end_cursor = end_cursor;
81 self
82 }
83
84 pub fn meta(&self, source: &str) -> DiagnosticMeta {
86 (self.formatter)(self, source)
87 }
88
89 pub fn span(&self) -> Span {
91 self.start_cursor.span() + self.end_cursor.span()
92 }
93
94 pub fn message(&self, source: &str) -> String {
96 self.meta(source).message
97 }
98
99 pub fn code(&self, source: &str) -> &'static str {
101 self.meta(source).code
102 }
103
104 pub fn help(&self, source: &str) -> String {
106 self.meta(source).help
107 }
108
109 pub fn with_desired_cursor(mut self, cursor: Cursor) -> Self {
111 self.desired_cursor = Some(cursor);
112 self
113 }
114
115 #[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 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 expected_int(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
140 DiagnosticMeta {
141 code: "ExpectedInt",
142 message: "This value isn't allowed to have a fraction, it must be a whole number.".into(),
143 help: "Try using a whole number instead".into(),
144 labels: vec![],
145 }
146 }
147
148 pub fn expected_unsigned(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
149 DiagnosticMeta {
150 code: "ExpectedUnsigned",
151 message: format!("Expected an unsigned number but saw `{}`", diagnostic.start_cursor.token().value()),
152 help: "This number cannot have a + or a -".into(),
153 labels: vec![],
154 }
155 }
156
157 pub fn unexpected_ident(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
158 let cursor = diagnostic.start_cursor;
159 let start = cursor.offset().0 as usize;
160 let len = cursor.token().len() as usize;
161 let message = if start + len <= source.len() {
162 let text = &source[start..start + len];
163 format!("Unexpected identifier '{text}'")
164 } else {
165 "Unexpected identifier".to_string()
166 };
167 DiagnosticMeta {
168 code: "UnexpectedIdent",
169 message,
170 help: "There is an extra word which shouldn't be in this position.".into(),
171 labels: vec![],
172 }
173 }
174
175 pub fn unexpected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
176 let cursor = diagnostic.start_cursor;
177 let message = if let Some(char) = cursor.token().char() {
178 format!("Unexpected delimiter '{char}'")
179 } else {
180 "Unexpected delimiter".to_string()
181 };
182 DiagnosticMeta { code: "UnexpectedDelim", message, help: "Try removing the character.".into(), labels: vec![] }
183 }
184
185 pub fn expected_ident(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
186 DiagnosticMeta {
187 code: "ExpectedIdent",
188 message: format!("Expected an identifier but found `{:?}`", Kind::from(diagnostic.start_cursor)),
189 help: "This is not correct CSS syntax.".into(),
190 labels: vec![],
191 }
192 }
193
194 pub fn expected_delim(diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
195 DiagnosticMeta {
196 code: "ExpectedDelim",
197 message: format!("Expected a delimiter but saw `{:?}`", Kind::from(diagnostic.start_cursor)),
198 help: "This is not correct CSS syntax.".into(),
199 labels: vec![],
200 }
201 }
202
203 pub fn bad_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
204 DiagnosticMeta {
205 code: "BadDeclaration",
206 message: "This declaration wasn't understood, and so was disregarded.".to_string(),
207 help: "The declaration contains invalid syntax, and will be ignored.".into(),
208 labels: vec![],
209 }
210 }
211
212 pub fn unknown_declaration(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
213 DiagnosticMeta {
214 code: "UnknownDeclaration",
215 message: "Ignored property due to parse error.".to_string(),
216 help: "This property is going to be ignored because it doesn't look valid. If it is valid, please file an issue!"
217 .into(),
218 labels: vec![],
219 }
220 }
221
222 pub fn expected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
223 DiagnosticMeta {
224 code: "ExpectedEnd",
225 message: "Expected this to be the end of the file, but there was more content.".to_string(),
226 help: "This is likely a problem with the parser. Please submit a bug report!".into(),
227 labels: vec![],
228 }
229 }
230
231 pub fn unexpected_end(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
232 DiagnosticMeta {
233 code: "UnexpectedEnd",
234 message: "Expected more content but reached the end of the file.".to_string(),
235 help: "Perhaps this file isn't finished yet?".into(),
236 labels: vec![],
237 }
238 }
239
240 pub fn unexpected_close_curly(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
241 DiagnosticMeta {
242 code: "UnexpectedCloseCurly",
243 message: "Expected more content before this curly brace.".to_string(),
244 help: "This needed more content here".into(),
245 labels: vec![],
246 }
247 }
248
249 pub fn unexpected_tag(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
250 let cursor = diagnostic.start_cursor;
251 let start = cursor.offset().0 as usize;
252 let len = cursor.token().len() as usize;
253 let message = if start + len <= source.len() {
254 let text = &source[start..start + len];
255 format!("Unexpected tag name '{text}'")
256 } else {
257 "Unexpected tag name".to_string()
258 };
259 DiagnosticMeta { code: "UnexpectedTag", message, help: "This isn't a valid tag name.".into(), labels: vec![] }
260 }
261
262 pub fn unexpected_id(diagnostic: &Diagnostic, source: &str) -> DiagnosticMeta {
263 let cursor = diagnostic.start_cursor;
264 let start = cursor.offset().0 as usize;
265 let len = cursor.token().len() as usize;
266 let message = if start + len <= source.len() {
267 let text = &source[start..start + len];
268 format!("Unexpected ID selector '{text}'")
269 } else {
270 "Unexpected ID selector".to_string()
271 };
272 DiagnosticMeta { code: "UnexpectedId", message, help: "This isn't a valid ID.".into(), labels: vec![] }
273 }
274
275 pub fn opentype_tag_length(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
276 DiagnosticMeta {
277 code: "OpentypeTagLength",
278 message: "OpenType tag must be exactly 4 characters".to_string(),
279 help: "Feature and variation axis tags like 'kern', 'liga', 'wght' are always 4 ASCII characters.".into(),
280 labels: vec![],
281 }
282 }
283
284 pub fn opentype_tag_ascii(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
285 DiagnosticMeta {
286 code: "OpentypeTagAscii",
287 message: "OpenType tag contains invalid characters".to_string(),
288 help: "Tags must only contain ASCII characters in the range U+20-7E (printable ASCII).".into(),
289 labels: vec![],
290 }
291 }
292
293 pub fn invalid_unicode_range(_diagnostic: &Diagnostic, _source: &str) -> DiagnosticMeta {
294 DiagnosticMeta {
295 code: "InvalidUnicodeRange",
296 message: "Invalid unicode-range".to_string(),
297 help: "A unicode range looks like `U+0-7F`, `U+30??` or `U+4E00-9FFF`.".into(),
298 labels: vec![],
299 }
300 }
301}