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 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}