mdbook_lint_core/
violation.rs

1//! Violation types for mdbook-lint
2//!
3//! This module contains the core types for representing linting violations.
4
5/// A violation found during linting
6#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
7pub struct Violation {
8    /// Rule identifier (e.g., "MD001")
9    pub rule_id: String,
10    /// Human-readable rule name (e.g., "heading-increment")
11    pub rule_name: String,
12    /// Description of the violation
13    pub message: String,
14    /// Line number (1-based)
15    pub line: usize,
16    /// Column number (1-based)
17    pub column: usize,
18    /// Severity level
19    pub severity: Severity,
20}
21
22/// Severity levels for violations
23#[derive(
24    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
25)]
26pub enum Severity {
27    /// Informational message
28    Info,
29    /// Warning that should be addressed
30    Warning,
31    /// Error that must be fixed
32    Error,
33}
34
35impl std::fmt::Display for Severity {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Severity::Info => write!(f, "info"),
39            Severity::Warning => write!(f, "warning"),
40            Severity::Error => write!(f, "error"),
41        }
42    }
43}
44
45impl std::fmt::Display for Violation {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(
48            f,
49            "{}:{}:{}: {}/{}: {}",
50            self.line, self.column, self.severity, self.rule_id, self.rule_name, self.message
51        )
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_severity_display() {
61        assert_eq!(format!("{}", Severity::Info), "info");
62        assert_eq!(format!("{}", Severity::Warning), "warning");
63        assert_eq!(format!("{}", Severity::Error), "error");
64    }
65
66    #[test]
67    fn test_severity_ordering() {
68        assert!(Severity::Info < Severity::Warning);
69        assert!(Severity::Warning < Severity::Error);
70        assert!(Severity::Info < Severity::Error);
71    }
72
73    #[test]
74    fn test_violation_creation() {
75        let violation = Violation {
76            rule_id: "MD001".to_string(),
77            rule_name: "heading-increment".to_string(),
78            message: "Heading levels should only increment by one level at a time".to_string(),
79            line: 5,
80            column: 1,
81            severity: Severity::Warning,
82        };
83
84        assert_eq!(violation.rule_id, "MD001");
85        assert_eq!(violation.rule_name, "heading-increment");
86        assert_eq!(violation.line, 5);
87        assert_eq!(violation.column, 1);
88        assert_eq!(violation.severity, Severity::Warning);
89    }
90
91    #[test]
92    fn test_violation_display() {
93        let violation = Violation {
94            rule_id: "MD013".to_string(),
95            rule_name: "line-length".to_string(),
96            message: "Line too long".to_string(),
97            line: 10,
98            column: 81,
99            severity: Severity::Error,
100        };
101
102        let expected = "10:81:error: MD013/line-length: Line too long";
103        assert_eq!(format!("{violation}"), expected);
104    }
105
106    #[test]
107    fn test_violation_equality() {
108        let violation1 = Violation {
109            rule_id: "MD001".to_string(),
110            rule_name: "heading-increment".to_string(),
111            message: "Test message".to_string(),
112            line: 1,
113            column: 1,
114            severity: Severity::Warning,
115        };
116
117        let violation2 = Violation {
118            rule_id: "MD001".to_string(),
119            rule_name: "heading-increment".to_string(),
120            message: "Test message".to_string(),
121            line: 1,
122            column: 1,
123            severity: Severity::Warning,
124        };
125
126        let violation3 = Violation {
127            rule_id: "MD002".to_string(),
128            rule_name: "first-heading-h1".to_string(),
129            message: "Different message".to_string(),
130            line: 2,
131            column: 1,
132            severity: Severity::Error,
133        };
134
135        assert_eq!(violation1, violation2);
136        assert_ne!(violation1, violation3);
137    }
138
139    #[test]
140    fn test_violation_clone() {
141        let original = Violation {
142            rule_id: "MD040".to_string(),
143            rule_name: "fenced-code-language".to_string(),
144            message: "Fenced code blocks should have a language specified".to_string(),
145            line: 15,
146            column: 3,
147            severity: Severity::Info,
148        };
149
150        let cloned = original.clone();
151        assert_eq!(original, cloned);
152    }
153
154    #[test]
155    fn test_violation_debug() {
156        let violation = Violation {
157            rule_id: "MD025".to_string(),
158            rule_name: "single-h1".to_string(),
159            message: "Multiple top level headings in the same document".to_string(),
160            line: 20,
161            column: 1,
162            severity: Severity::Warning,
163        };
164
165        let debug_str = format!("{violation:?}");
166        assert!(debug_str.contains("MD025"));
167        assert!(debug_str.contains("single-h1"));
168        assert!(debug_str.contains("Multiple top level headings"));
169        assert!(debug_str.contains("line: 20"));
170        assert!(debug_str.contains("column: 1"));
171        assert!(debug_str.contains("Warning"));
172    }
173
174    #[test]
175    fn test_all_severity_variants() {
176        let severities = [Severity::Info, Severity::Warning, Severity::Error];
177
178        for severity in &severities {
179            let violation = Violation {
180                rule_id: "TEST".to_string(),
181                rule_name: "test-rule".to_string(),
182                message: "Test message".to_string(),
183                line: 1,
184                column: 1,
185                severity: *severity,
186            };
187
188            // Test that display format includes severity
189            let display_str = format!("{violation}");
190            assert!(display_str.contains(&format!("{severity}")));
191        }
192    }
193}