ass_core/analysis/linting/rules/
missing_style.rs

1//! Missing style reference detection rule for ASS script linting.
2//!
3//! Detects events that reference undefined styles, which would cause
4//! rendering errors or fallback to default styling behavior.
5
6use crate::{
7    analysis::{
8        linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
9        ScriptAnalysis,
10    },
11    parser::Section,
12};
13use alloc::{format, vec::Vec};
14
15/// Rule for detecting events that reference undefined styles
16///
17/// Events must reference valid style names defined in the [V4+ Styles] section.
18/// Missing style references cause renderers to fall back to default styling,
19/// which may not match the intended visual appearance.
20///
21/// # Performance
22///
23/// - Time complexity: O(n + m) for n styles and m events
24/// - Memory: O(n) for style name collection
25/// - Target: <1ms for typical scripts with 100 styles and 1000 events
26///
27/// # Example
28///
29/// ```rust
30/// use ass_core::analysis::linting::rules::missing_style::MissingStyleRule;
31/// use ass_core::analysis::linting::LintRule;
32/// use ass_core::{Script, ScriptAnalysis};
33///
34/// let script = Script::parse(r#"
35/// [V4+ Styles]
36/// Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
37/// Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
38///
39/// [Events]
40/// Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
41/// Dialogue: 0,0:00:00.00,0:00:05.00,Undefined,,0,0,0,,Text with undefined style
42/// "#)?;
43///
44/// let analysis = ScriptAnalysis::analyze(&script)?;
45/// let rule = MissingStyleRule;
46/// let issues = rule.check_script(&analysis);
47/// assert!(!issues.is_empty()); // Should detect the missing style reference
48/// # Ok::<(), Box<dyn std::error::Error>>(())
49/// ```
50pub struct MissingStyleRule;
51
52impl LintRule for MissingStyleRule {
53    fn id(&self) -> &'static str {
54        "missing-style"
55    }
56
57    fn name(&self) -> &'static str {
58        "Missing Style"
59    }
60
61    fn description(&self) -> &'static str {
62        "Detects events referencing non-existent styles"
63    }
64
65    fn default_severity(&self) -> IssueSeverity {
66        IssueSeverity::Error
67    }
68
69    fn category(&self) -> IssueCategory {
70        IssueCategory::Styling
71    }
72
73    fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
74        let mut issues = Vec::new();
75
76        let style_names: Vec<&str> = analysis
77            .resolved_styles()
78            .iter()
79            .map(|style| style.name)
80            .collect();
81
82        if let Some(Section::Events(events)) = analysis
83            .script()
84            .sections()
85            .iter()
86            .find(|s| matches!(s, Section::Events(_)))
87        {
88            for event in events {
89                if !style_names.contains(&event.style) {
90                    let issue = LintIssue::new(
91                        self.default_severity(),
92                        IssueCategory::Styling,
93                        self.id(),
94                        format!("Event references undefined style: {}", event.style),
95                    )
96                    .with_suggested_fix(format!(
97                        "Define style '{}' or use an existing style",
98                        event.style
99                    ));
100
101                    issues.push(issue);
102                }
103            }
104        }
105
106        issues
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn rule_metadata_correct() {
116        let rule = MissingStyleRule;
117        assert_eq!(rule.id(), "missing-style");
118        assert_eq!(rule.name(), "Missing Style");
119        assert_eq!(
120            rule.description(),
121            "Detects events referencing non-existent styles"
122        );
123        assert_eq!(rule.default_severity(), IssueSeverity::Error);
124        assert_eq!(rule.category(), IssueCategory::Styling);
125    }
126
127    #[test]
128    fn empty_script_no_issues() {
129        let script_text = "[Script Info]\nTitle: Test";
130        let script = crate::parser::Script::parse(script_text).unwrap();
131        let analysis = ScriptAnalysis::analyze(&script).unwrap();
132
133        let rule = MissingStyleRule;
134        let issues = rule.check_script(&analysis);
135
136        assert!(issues.is_empty());
137    }
138
139    #[test]
140    fn valid_style_reference_no_issues() {
141        let script_text = r"[V4+ Styles]
142Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
143Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
144
145[Events]
146Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
147Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Valid event";
148
149        let script = crate::parser::Script::parse(script_text).unwrap();
150        let analysis = ScriptAnalysis::analyze(&script).unwrap();
151        let rule = MissingStyleRule;
152        let issues = rule.check_script(&analysis);
153
154        assert!(issues.is_empty());
155    }
156
157    #[test]
158    fn missing_style_reference_detected() {
159        let script_text = r"[V4+ Styles]
160Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
161Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
162
163[Events]
164Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
165Dialogue: 0,0:00:00.00,0:00:05.00,Undefined,,0,0,0,,Invalid event";
166
167        let script = crate::parser::Script::parse(script_text).unwrap();
168        let analysis = ScriptAnalysis::analyze(&script).unwrap();
169        let rule = MissingStyleRule;
170        let issues = rule.check_script(&analysis);
171
172        assert_eq!(issues.len(), 1);
173        assert_eq!(issues[0].severity(), IssueSeverity::Error);
174        assert_eq!(issues[0].category(), IssueCategory::Styling);
175        assert!(issues[0].message().contains("Undefined"));
176    }
177
178    #[test]
179    fn multiple_missing_styles() {
180        let script_text = r"[V4+ Styles]
181Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
182Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
183
184[Events]
185Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
186Dialogue: 0,0:00:00.00,0:00:05.00,Missing1,,0,0,0,,First invalid
187Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Valid event
188Dialogue: 0,0:00:10.00,0:00:15.00,Missing2,,0,0,0,,Second invalid";
189
190        let script = crate::parser::Script::parse(script_text).unwrap();
191        let analysis = ScriptAnalysis::analyze(&script).unwrap();
192        let rule = MissingStyleRule;
193        let issues = rule.check_script(&analysis);
194
195        assert_eq!(issues.len(), 2);
196    }
197
198    #[test]
199    fn no_styles_section_all_invalid() {
200        let script_text = r"[Events]
201Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
202Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Should be invalid";
203
204        let script = crate::parser::Script::parse(script_text).unwrap();
205        let analysis = ScriptAnalysis::analyze(&script).unwrap();
206        let rule = MissingStyleRule;
207        let issues = rule.check_script(&analysis);
208
209        assert_eq!(issues.len(), 1);
210    }
211}