ass_core/analysis/linting/rules/
negative_duration.rs1use crate::{
7 analysis::{
8 linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
9 ScriptAnalysis,
10 },
11 parser::Section,
12 utils::parse_ass_time,
13};
14use alloc::{format, string::ToString, vec::Vec};
15
16pub struct NegativeDurationRule;
48
49impl LintRule for NegativeDurationRule {
50 fn id(&self) -> &'static str {
51 "negative-duration"
52 }
53
54 fn name(&self) -> &'static str {
55 "Negative Duration"
56 }
57
58 fn description(&self) -> &'static str {
59 "Detects events with negative or zero duration"
60 }
61
62 fn default_severity(&self) -> IssueSeverity {
63 IssueSeverity::Error
64 }
65
66 fn category(&self) -> IssueCategory {
67 IssueCategory::Timing
68 }
69
70 fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
71 let mut issues = Vec::new();
72
73 if let Some(Section::Events(events)) = analysis
74 .script()
75 .sections()
76 .iter()
77 .find(|s| matches!(s, Section::Events(_)))
78 {
79 for event in events {
80 if let (Ok(start), Ok(end)) =
81 (parse_ass_time(event.start), parse_ass_time(event.end))
82 {
83 if start >= end {
84 let issue = LintIssue::new(
85 self.default_severity(),
86 IssueCategory::Timing,
87 self.id(),
88 format!(
89 "Invalid duration: start {} >= end {}",
90 event.start, event.end
91 ),
92 )
93 .with_description(
94 "Events must have positive duration for proper display".to_string(),
95 );
96
97 issues.push(issue);
98 }
99 }
100 }
101 }
102
103 issues
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn rule_metadata_correct() {
113 let rule = NegativeDurationRule;
114 assert_eq!(rule.id(), "negative-duration");
115 assert_eq!(rule.name(), "Negative Duration");
116 assert_eq!(
117 rule.description(),
118 "Detects events with negative or zero duration"
119 );
120 assert_eq!(rule.default_severity(), IssueSeverity::Error);
121 assert_eq!(rule.category(), IssueCategory::Timing);
122 }
123
124 #[test]
125 fn empty_script_no_issues() {
126 let script_text = "[Script Info]\nTitle: Test";
127 let script = crate::parser::Script::parse(script_text).unwrap();
128 let analysis = ScriptAnalysis::analyze(&script).unwrap();
129
130 let rule = NegativeDurationRule;
131 let issues = rule.check_script(&analysis);
132
133 assert!(issues.is_empty());
134 }
135
136 #[test]
137 fn valid_duration_no_issues() {
138 let script_text = r"[Events]
139Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
140Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Valid event";
141
142 let script = crate::parser::Script::parse(script_text).unwrap();
143 let analysis = ScriptAnalysis::analyze(&script).unwrap();
144 let rule = NegativeDurationRule;
145 let issues = rule.check_script(&analysis);
146
147 assert!(issues.is_empty());
148 }
149
150 #[test]
151 fn negative_duration_detected() {
152 let script_text = r"[Events]
153Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
154Dialogue: 0,0:00:05.00,0:00:02.00,Default,,0,0,0,,Invalid event";
155
156 let script = crate::parser::Script::parse(script_text).unwrap();
157 let analysis = ScriptAnalysis::analyze(&script).unwrap();
158 let rule = NegativeDurationRule;
159 let issues = rule.check_script(&analysis);
160
161 assert_eq!(issues.len(), 1);
162 assert_eq!(issues[0].severity(), IssueSeverity::Error);
163 assert_eq!(issues[0].category(), IssueCategory::Timing);
164 }
165
166 #[test]
167 fn zero_duration_detected() {
168 let script_text = r"[Events]
169Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
170Dialogue: 0,0:00:05.00,0:00:05.00,Default,,0,0,0,,Zero duration";
171
172 let script = crate::parser::Script::parse(script_text).unwrap();
173 let analysis = ScriptAnalysis::analyze(&script).unwrap();
174 let rule = NegativeDurationRule;
175 let issues = rule.check_script(&analysis);
176
177 assert_eq!(issues.len(), 1);
178 assert_eq!(issues[0].severity(), IssueSeverity::Error);
179 }
180
181 #[test]
182 fn multiple_invalid_durations() {
183 let script_text = r"[Events]
184Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
185Dialogue: 0,0:00:05.00,0:00:02.00,Default,,0,0,0,,First invalid
186Dialogue: 0,0:00:01.00,0:00:06.00,Default,,0,0,0,,Valid event
187Dialogue: 0,0:00:10.00,0:00:10.00,Default,,0,0,0,,Second invalid";
188
189 let script = crate::parser::Script::parse(script_text).unwrap();
190 let analysis = ScriptAnalysis::analyze(&script).unwrap();
191 let rule = NegativeDurationRule;
192 let issues = rule.check_script(&analysis);
193
194 assert_eq!(issues.len(), 2);
195 }
196}