ass_core/analysis/linting/rules/mod.rs
1//! Built-in linting rules for ASS script validation.
2//!
3//! This module contains implementations of all built-in linting rules
4//! that check for common issues in ASS subtitle scripts. Each rule
5//! is implemented in a separate module for better maintainability.
6//!
7//! # Rule Categories
8//!
9//! - **Timing Rules**: Check for overlaps, negative durations, and timing issues
10//! - **Style Rules**: Validate style references and color formats
11//! - **Content Rules**: Check tag validity and text formatting
12//! - **Performance Rules**: Detect performance-impacting patterns
13//! - **Accessibility Rules**: Ensure compatibility and readability
14//!
15//! # Example
16//!
17//! ```rust
18//! use ass_core::analysis::linting::rules::BuiltinRules;
19//! use ass_core::analysis::linting::LintRule;
20//! use ass_core::{Script, ScriptAnalysis};
21//!
22//! let script = Script::parse("...")?;
23//! let rules = BuiltinRules::all_rules();
24//!
25//! for rule in rules {
26//! let analysis = ScriptAnalysis::analyze(&script).unwrap();
27//! let issues = rule.check_script(&analysis);
28//! for issue in issues {
29//! println!("{}: {}", rule.name(), issue.message());
30//! }
31//! }
32//! # Ok::<(), Box<dyn std::error::Error>>(())
33//! ```
34
35use super::LintRule;
36use alloc::{boxed::Box, vec, vec::Vec};
37
38pub mod accessibility;
39pub mod encoding;
40pub mod invalid_color;
41pub mod invalid_tag;
42pub mod missing_style;
43pub mod negative_duration;
44pub mod performance;
45pub mod timing_overlap;
46
47pub use accessibility::AccessibilityRule;
48pub use encoding::EncodingRule;
49pub use invalid_color::InvalidColorRule;
50pub use invalid_tag::InvalidTagRule;
51pub use missing_style::MissingStyleRule;
52pub use negative_duration::NegativeDurationRule;
53pub use performance::PerformanceRule;
54pub use timing_overlap::TimingOverlapRule;
55
56/// Built-in lint rules registry
57///
58/// Provides access to all built-in rules that check for common issues
59/// in ASS subtitle scripts. Rules are organized by category and can be
60/// used individually or as a complete set.
61///
62/// # Performance
63///
64/// All rules are designed for efficient execution with minimal memory
65/// overhead. Most rules have O(n) or O(n log n) time complexity.
66///
67/// # Rule List
68///
69/// - `TimingOverlapRule`: Detects overlapping dialogue events
70/// - `NegativeDurationRule`: Finds events with invalid durations
71/// - `InvalidColorRule`: Validates color formats in styles and tags
72/// - `MissingStyleRule`: Checks for undefined style references
73/// - `InvalidTagRule`: Detects malformed override tags
74/// - `PerformanceRule`: Identifies performance-impacting patterns
75/// - `EncodingRule`: Validates text encoding and character usage
76/// - `AccessibilityRule`: Ensures readability and compatibility
77pub struct BuiltinRules;
78
79impl BuiltinRules {
80 /// Get all built-in linting rules
81 ///
82 /// Returns a vector of all available built-in rules ready for use.
83 /// Rules are returned in their default configuration with standard
84 /// severity levels and categories.
85 ///
86 /// # Example
87 ///
88 /// ```rust
89 /// use ass_core::analysis::linting::rules::BuiltinRules;
90 ///
91 /// let rules = BuiltinRules::all_rules();
92 /// assert_eq!(rules.len(), 8); // All built-in rules
93 /// ```
94 #[must_use]
95 pub fn all_rules() -> Vec<Box<dyn LintRule>> {
96 vec![
97 Box::new(TimingOverlapRule),
98 Box::new(NegativeDurationRule),
99 Box::new(InvalidColorRule),
100 Box::new(MissingStyleRule),
101 Box::new(InvalidTagRule),
102 Box::new(PerformanceRule),
103 Box::new(EncodingRule),
104 Box::new(AccessibilityRule),
105 ]
106 }
107
108 /// Get rules by category
109 ///
110 /// Returns only rules that check issues in the specified category.
111 /// Useful for focused linting or when only certain types of issues
112 /// need to be checked.
113 ///
114 /// # Arguments
115 ///
116 /// * `category` - The issue category to filter by
117 ///
118 /// # Example
119 ///
120 /// ```rust
121 /// use ass_core::analysis::linting::{IssueCategory, rules::BuiltinRules};
122 ///
123 /// let timing_rules = BuiltinRules::rules_for_category(IssueCategory::Timing);
124 /// // Returns timing-related rules only
125 /// ```
126 #[must_use]
127 pub fn rules_for_category(category: super::IssueCategory) -> Vec<Box<dyn LintRule>> {
128 Self::all_rules()
129 .into_iter()
130 .filter(|rule| rule.category() == category)
131 .collect()
132 }
133
134 /// Get rule by ID
135 ///
136 /// Returns the rule with the specified ID, or None if no such rule exists.
137 /// Rule IDs are unique identifiers used for configuration and reporting.
138 ///
139 /// # Arguments
140 ///
141 /// * `id` - The rule ID to search for
142 ///
143 /// # Example
144 ///
145 /// ```rust
146 /// use ass_core::analysis::linting::rules::BuiltinRules;
147 ///
148 /// let rule = BuiltinRules::rule_by_id("timing-overlap");
149 /// assert!(rule.is_some());
150 /// assert_eq!(rule.unwrap().id(), "timing-overlap");
151 /// ```
152 #[must_use]
153 pub fn rule_by_id(id: &str) -> Option<Box<dyn LintRule>> {
154 Self::all_rules().into_iter().find(|rule| rule.id() == id)
155 }
156
157 /// Get all rule IDs
158 ///
159 /// Returns a vector of all available rule IDs for configuration
160 /// and reporting purposes.
161 ///
162 /// # Example
163 ///
164 /// ```rust
165 /// use ass_core::analysis::linting::rules::BuiltinRules;
166 ///
167 /// let ids = BuiltinRules::all_rule_ids();
168 /// assert!(ids.contains(&"timing-overlap"));
169 /// assert!(ids.contains(&"negative-duration"));
170 /// ```
171 #[must_use]
172 pub fn all_rule_ids() -> Vec<&'static str> {
173 Self::all_rules().iter().map(|rule| rule.id()).collect()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn all_rules_count_correct() {
183 let rules = BuiltinRules::all_rules();
184 assert_eq!(rules.len(), 8);
185 }
186
187 #[test]
188 fn all_rules_have_unique_ids() {
189 let rules = BuiltinRules::all_rules();
190 let mut ids = Vec::new();
191
192 for rule in rules {
193 let id = rule.id();
194 assert!(!ids.contains(&id), "Duplicate rule ID: {id}");
195 ids.push(id);
196 }
197 }
198
199 #[test]
200 fn rule_by_id_works() {
201 let rule = BuiltinRules::rule_by_id("timing-overlap");
202 assert!(rule.is_some());
203 assert_eq!(rule.unwrap().id(), "timing-overlap");
204
205 let missing = BuiltinRules::rule_by_id("nonexistent");
206 assert!(missing.is_none());
207 }
208
209 #[test]
210 fn all_rule_ids_complete() {
211 let ids = BuiltinRules::all_rule_ids();
212 let expected_ids = [
213 "timing-overlap",
214 "negative-duration",
215 "invalid-color",
216 "missing-style",
217 "invalid-tag",
218 "performance",
219 "encoding",
220 "accessibility",
221 ];
222
223 for expected_id in expected_ids {
224 assert!(ids.contains(&expected_id), "Missing rule ID: {expected_id}");
225 }
226 }
227}