ass_core/analysis/linting/rules/
timing_overlap.rs1use crate::{
7 analysis::{
8 events::find_overlapping_events,
9 linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
10 ScriptAnalysis,
11 },
12 parser::Section,
13};
14use alloc::{format, string::ToString, vec::Vec};
15
16pub struct TimingOverlapRule;
49
50impl LintRule for TimingOverlapRule {
51 fn id(&self) -> &'static str {
52 "timing-overlap"
53 }
54
55 fn name(&self) -> &'static str {
56 "Timing Overlap"
57 }
58
59 fn description(&self) -> &'static str {
60 "Detects overlapping dialogue events that may cause rendering conflicts"
61 }
62
63 fn default_severity(&self) -> IssueSeverity {
64 IssueSeverity::Warning
65 }
66
67 fn category(&self) -> IssueCategory {
68 IssueCategory::Timing
69 }
70
71 fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
72 let mut issues = Vec::new();
73
74 if let Some(Section::Events(events)) = analysis
75 .script()
76 .sections()
77 .iter()
78 .find(|s| matches!(s, Section::Events(_)))
79 {
80 if let Ok(overlaps) = find_overlapping_events(events) {
81 for (i, j) in overlaps {
82 let event1 = &events[i];
83 let event2 = &events[j];
84
85 let issue = LintIssue::new(
86 self.default_severity(),
87 IssueCategory::Timing,
88 self.id(),
89 format!(
90 "Event overlaps: {} to {} overlaps with {} to {}",
91 event1.start, event1.end, event2.start, event2.end
92 ),
93 )
94 .with_description(
95 "Overlapping events may cause rendering conflicts".to_string(),
96 );
97
98 issues.push(issue);
99 }
100 } else {
101 let issue = LintIssue::new(
102 IssueSeverity::Warning,
103 IssueCategory::Timing,
104 self.id(),
105 "Could not analyze event overlaps due to timing parse errors".to_string(),
106 );
107 issues.push(issue);
108 }
109 }
110
111 issues
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn rule_metadata_correct() {
121 let rule = TimingOverlapRule;
122 assert_eq!(rule.id(), "timing-overlap");
123 assert_eq!(rule.name(), "Timing Overlap");
124 assert_eq!(
125 rule.description(),
126 "Detects overlapping dialogue events that may cause rendering conflicts"
127 );
128 assert_eq!(rule.default_severity(), IssueSeverity::Warning);
129 assert_eq!(rule.category(), IssueCategory::Timing);
130 }
131
132 #[test]
133 fn empty_script_no_issues() {
134 let script_text = "[Script Info]\nTitle: Test";
135 let script = crate::parser::Script::parse(script_text).unwrap();
136 let analysis = ScriptAnalysis::analyze(&script).unwrap();
137
138 let rule = TimingOverlapRule;
139 let issues = rule.check_script(&analysis);
140
141 assert!(issues.is_empty());
142 }
143
144 #[test]
145 fn non_overlapping_events_no_issues() {
146 let script_text = r"[Events]
147Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
148Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event
149Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event";
150
151 let script = crate::parser::Script::parse(script_text).unwrap();
152 let analysis = ScriptAnalysis::analyze(&script).unwrap();
153 let rule = TimingOverlapRule;
154 let issues = rule.check_script(&analysis);
155
156 assert!(issues.is_empty());
157 }
158}