ass_core/parser/errors/
parse_issue.rs

1//! Parse issue types for recoverable parsing problems
2//!
3//! Contains types for representing warnings, errors, and other issues that
4//! can be recovered from during parsing. These allow continued parsing
5//! while collecting problems for later review.
6
7use alloc::{format, string::String};
8use core::fmt;
9
10#[cfg(not(feature = "std"))]
11extern crate alloc;
12/// Parse issue severity levels for partial recovery
13///
14/// Determines how serious an issue is and whether it should block processing.
15/// Lower severity issues can often be ignored or worked around.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum IssueSeverity {
18    /// Information that may be useful but doesn't affect functionality
19    Info,
20
21    /// Warning about potential problems or non-standard usage
22    Warning,
23
24    /// Error that was recovered from but may affect rendering
25    Error,
26
27    /// Critical error that will likely cause rendering problems
28    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/// Issue categories for filtering and editor integration
43///
44/// Groups related issues together for easier filtering and handling.
45/// Useful for editor extensions and linting tool integration.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum IssueCategory {
48    /// Script structure issues
49    Structure,
50
51    /// Style definition problems
52    Style,
53
54    /// Event/dialogue issues
55    Event,
56
57    /// Timing-related problems
58    Timing,
59
60    /// Color format issues
61    Color,
62
63    /// Font/typography issues
64    Font,
65
66    /// Drawing command problems
67    Drawing,
68
69    /// Performance warnings
70    Performance,
71
72    /// Compatibility warnings
73    Compatibility,
74
75    /// Security warnings
76    Security,
77
78    /// General format issues
79    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/// Parse issue for recoverable problems and warnings
101///
102/// Used for problems that don't prevent parsing but may affect
103/// rendering quality or indicate potential script issues.
104/// Includes location information for editor integration.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ParseIssue {
107    /// Issue severity level
108    pub severity: IssueSeverity,
109
110    /// Issue category for filtering/grouping
111    pub category: IssueCategory,
112
113    /// Human-readable message
114    pub message: String,
115
116    /// Line number where issue occurred (1-based)
117    pub line: usize,
118
119    /// Column number where issue occurred (1-based)
120    pub column: Option<usize>,
121
122    /// Byte range in source where issue occurred
123    pub span: Option<(usize, usize)>,
124
125    /// Suggested fix or explanation
126    pub suggestion: Option<String>,
127}
128
129impl ParseIssue {
130    /// Create new parse issue with minimal information
131    #[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    /// Create issue with full location information
150    #[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    /// Add suggestion to existing issue
171    #[must_use]
172    pub fn with_suggestion(mut self, suggestion: String) -> Self {
173        self.suggestion = Some(suggestion);
174        self
175    }
176
177    /// Create info-level issue
178    #[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    /// Create warning-level issue
184    #[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    /// Create error-level issue
190    #[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    /// Create critical-level issue
196    #[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    /// Format issue for display in editor or console
202    #[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    /// Check if this is a blocking error that should prevent further processing
223    #[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}