1use alloc::{format, string::String};
8use core::fmt;
9
10#[cfg(not(feature = "std"))]
11extern crate alloc;
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum IssueSeverity {
18 Info,
20
21 Warning,
23
24 Error,
26
27 Critical,
29}
30
31impl fmt::Display for IssueSeverity {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::Info => write!(f, "info"),
35 Self::Warning => write!(f, "warning"),
36 Self::Error => write!(f, "error"),
37 Self::Critical => write!(f, "critical"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum IssueCategory {
48 Structure,
50
51 Style,
53
54 Event,
56
57 Timing,
59
60 Color,
62
63 Font,
65
66 Drawing,
68
69 Performance,
71
72 Compatibility,
74
75 Security,
77
78 Format,
80}
81
82impl fmt::Display for IssueCategory {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Structure => write!(f, "structure"),
86 Self::Style => write!(f, "style"),
87 Self::Event => write!(f, "event"),
88 Self::Timing => write!(f, "timing"),
89 Self::Color => write!(f, "color"),
90 Self::Font => write!(f, "font"),
91 Self::Drawing => write!(f, "drawing"),
92 Self::Performance => write!(f, "performance"),
93 Self::Compatibility => write!(f, "compatibility"),
94 Self::Security => write!(f, "security"),
95 Self::Format => write!(f, "format"),
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ParseIssue {
107 pub severity: IssueSeverity,
109
110 pub category: IssueCategory,
112
113 pub message: String,
115
116 pub line: usize,
118
119 pub column: Option<usize>,
121
122 pub span: Option<(usize, usize)>,
124
125 pub suggestion: Option<String>,
127}
128
129impl ParseIssue {
130 #[must_use]
132 pub const fn new(
133 severity: IssueSeverity,
134 category: IssueCategory,
135 message: String,
136 line: usize,
137 ) -> Self {
138 Self {
139 severity,
140 category,
141 message,
142 line,
143 column: None,
144 span: None,
145 suggestion: None,
146 }
147 }
148
149 #[must_use]
151 pub const fn with_location(
152 severity: IssueSeverity,
153 category: IssueCategory,
154 message: String,
155 line: usize,
156 column: usize,
157 span: (usize, usize),
158 ) -> Self {
159 Self {
160 severity,
161 category,
162 message,
163 line,
164 column: Some(column),
165 span: Some(span),
166 suggestion: None,
167 }
168 }
169
170 #[must_use]
172 pub fn with_suggestion(mut self, suggestion: String) -> Self {
173 self.suggestion = Some(suggestion);
174 self
175 }
176
177 #[must_use]
179 pub const fn info(category: IssueCategory, message: String, line: usize) -> Self {
180 Self::new(IssueSeverity::Info, category, message, line)
181 }
182
183 #[must_use]
185 pub const fn warning(category: IssueCategory, message: String, line: usize) -> Self {
186 Self::new(IssueSeverity::Warning, category, message, line)
187 }
188
189 #[must_use]
191 pub const fn error(category: IssueCategory, message: String, line: usize) -> Self {
192 Self::new(IssueSeverity::Error, category, message, line)
193 }
194
195 #[must_use]
197 pub const fn critical(category: IssueCategory, message: String, line: usize) -> Self {
198 Self::new(IssueSeverity::Critical, category, message, line)
199 }
200
201 #[must_use]
203 pub fn format_for_display(&self) -> String {
204 let location = self.column.map_or_else(
205 || format!("{}", self.line),
206 |column| format!("{}:{}", self.line, column),
207 );
208
209 let mut result = format!(
210 "[{}:{}] {}: {}",
211 location, self.category, self.severity, self.message
212 );
213
214 if let Some(suggestion) = &self.suggestion {
215 result.push_str("\n Suggestion: ");
216 result.push_str(suggestion);
217 }
218
219 result
220 }
221
222 #[must_use]
224 pub const fn is_blocking(&self) -> bool {
225 matches!(self.severity, IssueSeverity::Critical)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 #[cfg(not(feature = "std"))]
233 use alloc::string::ToString;
234
235 #[test]
236 fn issue_severity_display() {
237 assert_eq!(format!("{}", IssueSeverity::Info), "info");
238 assert_eq!(format!("{}", IssueSeverity::Warning), "warning");
239 assert_eq!(format!("{}", IssueSeverity::Error), "error");
240 assert_eq!(format!("{}", IssueSeverity::Critical), "critical");
241 }
242
243 #[test]
244 fn issue_category_display() {
245 assert_eq!(format!("{}", IssueCategory::Structure), "structure");
246 assert_eq!(format!("{}", IssueCategory::Style), "style");
247 assert_eq!(format!("{}", IssueCategory::Event), "event");
248 assert_eq!(format!("{}", IssueCategory::Timing), "timing");
249 assert_eq!(format!("{}", IssueCategory::Color), "color");
250 assert_eq!(format!("{}", IssueCategory::Font), "font");
251 assert_eq!(format!("{}", IssueCategory::Drawing), "drawing");
252 assert_eq!(format!("{}", IssueCategory::Performance), "performance");
253 assert_eq!(format!("{}", IssueCategory::Compatibility), "compatibility");
254 assert_eq!(format!("{}", IssueCategory::Security), "security");
255 assert_eq!(format!("{}", IssueCategory::Format), "format");
256 }
257
258 #[test]
259 fn parse_issue_creation() {
260 let issue = ParseIssue::new(
261 IssueSeverity::Warning,
262 IssueCategory::Style,
263 "Negative font size".to_string(),
264 10,
265 );
266
267 assert_eq!(issue.severity, IssueSeverity::Warning);
268 assert_eq!(issue.category, IssueCategory::Style);
269 assert_eq!(issue.message, "Negative font size");
270 assert_eq!(issue.line, 10);
271 assert_eq!(issue.column, None);
272 assert_eq!(issue.span, None);
273 assert_eq!(issue.suggestion, None);
274 assert!(!issue.is_blocking());
275 }
276
277 #[test]
278 fn parse_issue_with_location() {
279 let issue = ParseIssue::with_location(
280 IssueSeverity::Error,
281 IssueCategory::Color,
282 "Invalid color format".to_string(),
283 15,
284 25,
285 (100, 110),
286 );
287
288 assert_eq!(issue.severity, IssueSeverity::Error);
289 assert_eq!(issue.category, IssueCategory::Color);
290 assert_eq!(issue.line, 15);
291 assert_eq!(issue.column, Some(25));
292 assert_eq!(issue.span, Some((100, 110)));
293 assert!(!issue.is_blocking());
294 }
295
296 #[test]
297 fn parse_issue_with_suggestion() {
298 let issue = ParseIssue::error(
299 IssueCategory::Format,
300 "Missing colon in field".to_string(),
301 8,
302 )
303 .with_suggestion("Add ':' after field name".to_string());
304
305 assert_eq!(issue.severity, IssueSeverity::Error);
306 assert!(issue.suggestion.is_some());
307 assert_eq!(issue.suggestion.unwrap(), "Add ':' after field name");
308 }
309
310 #[test]
311 fn parse_issue_convenience_constructors() {
312 let info_issue =
313 ParseIssue::info(IssueCategory::Performance, "Info message".to_string(), 1);
314 assert_eq!(info_issue.severity, IssueSeverity::Info);
315 assert!(!info_issue.is_blocking());
316
317 let warning_issue =
318 ParseIssue::warning(IssueCategory::Style, "Warning message".to_string(), 2);
319 assert_eq!(warning_issue.severity, IssueSeverity::Warning);
320 assert!(!warning_issue.is_blocking());
321
322 let error_issue = ParseIssue::error(IssueCategory::Color, "Error message".to_string(), 3);
323 assert_eq!(error_issue.severity, IssueSeverity::Error);
324 assert!(!error_issue.is_blocking());
325
326 let critical_issue =
327 ParseIssue::critical(IssueCategory::Structure, "Critical message".to_string(), 4);
328 assert_eq!(critical_issue.severity, IssueSeverity::Critical);
329 assert!(critical_issue.is_blocking());
330 }
331
332 #[test]
333 fn parse_issue_formatting_simple() {
334 let issue = ParseIssue::warning(
335 IssueCategory::Performance,
336 "Many overlapping events".to_string(),
337 20,
338 );
339
340 let formatted = issue.format_for_display();
341 assert!(formatted.contains("20"));
342 assert!(formatted.contains("performance"));
343 assert!(formatted.contains("warning"));
344 assert!(formatted.contains("Many overlapping events"));
345 assert!(!formatted.contains("Suggestion:"));
346 }
347
348 #[test]
349 fn parse_issue_formatting_with_location() {
350 let issue = ParseIssue::with_location(
351 IssueSeverity::Error,
352 IssueCategory::Timing,
353 "Overlapping dialogue".to_string(),
354 30,
355 15,
356 (200, 250),
357 );
358
359 let formatted = issue.format_for_display();
360 assert!(formatted.contains("30:15"));
361 assert!(formatted.contains("timing"));
362 assert!(formatted.contains("error"));
363 assert!(formatted.contains("Overlapping dialogue"));
364 }
365
366 #[test]
367 fn parse_issue_formatting_with_suggestion() {
368 let issue = ParseIssue::with_location(
369 IssueSeverity::Warning,
370 IssueCategory::Performance,
371 "Many override tags".to_string(),
372 40,
373 5,
374 (300, 350),
375 )
376 .with_suggestion("Consider using styles instead".to_string());
377
378 let formatted = issue.format_for_display();
379 assert!(formatted.contains("40:5"));
380 assert!(formatted.contains("performance"));
381 assert!(formatted.contains("warning"));
382 assert!(formatted.contains("Many override tags"));
383 assert!(formatted.contains("Suggestion:"));
384 assert!(formatted.contains("Consider using styles instead"));
385 }
386
387 #[test]
388 fn parse_issue_blocking_detection() {
389 let non_blocking_info = ParseIssue::info(IssueCategory::Format, "Info".to_string(), 1);
390 let non_blocking_warning =
391 ParseIssue::warning(IssueCategory::Style, "Warning".to_string(), 2);
392 let non_blocking_error = ParseIssue::error(IssueCategory::Color, "Error".to_string(), 3);
393 let blocking_critical =
394 ParseIssue::critical(IssueCategory::Structure, "Critical".to_string(), 4);
395
396 assert!(!non_blocking_info.is_blocking());
397 assert!(!non_blocking_warning.is_blocking());
398 assert!(!non_blocking_error.is_blocking());
399 assert!(blocking_critical.is_blocking());
400 }
401
402 #[test]
403 fn parse_issue_clone_and_equality() {
404 let issue1 = ParseIssue::warning(IssueCategory::Font, "Missing font".to_string(), 50);
405 let issue2 = issue1.clone();
406 assert_eq!(issue1, issue2);
407
408 let issue3 = ParseIssue::warning(IssueCategory::Font, "Different message".to_string(), 50);
409 assert_ne!(issue1, issue3);
410 }
411
412 #[test]
413 fn parse_issue_debug() {
414 let issue = ParseIssue::error(IssueCategory::Drawing, "Invalid command".to_string(), 60);
415 let debug_str = format!("{issue:?}");
416 assert!(debug_str.contains("ParseIssue"));
417 assert!(debug_str.contains("Error"));
418 assert!(debug_str.contains("Drawing"));
419 }
420}