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 message(&self, source: &str) -> String {
86 let DiagnosticMeta { message, .. } = (self.formatter)(self, source);
87 message
88 }
89
90 pub fn code(&self, source: &str) -> &'static str {
92 let DiagnosticMeta { code, .. } = (self.formatter)(self, source);
93 code
94 }
95
96 pub fn help(&self, source: &str) -> String {
98 let DiagnosticMeta { help, .. } = (self.formatter)(self, source);
99 help
100 }
101
102 pub fn with_desired_cursor(mut self, cursor: Cursor) -> Self {
104 self.desired_cursor = Some(cursor);
105 self
106 }
107
108 #[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 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}