1use crate::{
7 analysis::{
8 linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
9 ScriptAnalysis,
10 },
11 parser::Section,
12};
13use alloc::{format, vec::Vec};
14
15pub 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}