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}