1#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
7pub struct Fix {
8 pub description: String,
10 pub replacement: Option<String>,
12 pub start: Position,
14 pub end: Position,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
20pub struct Position {
21 pub line: usize,
23 pub column: usize,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
29pub struct Violation {
30 pub rule_id: String,
32 pub rule_name: String,
34 pub message: String,
36 pub line: usize,
38 pub column: usize,
40 pub severity: Severity,
42 pub fix: Option<Fix>,
44}
45
46#[derive(
48 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
49)]
50pub enum Severity {
51 Info,
53 Warning,
55 Error,
57}
58
59impl std::fmt::Display for Severity {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Severity::Info => write!(f, "info"),
63 Severity::Warning => write!(f, "warning"),
64 Severity::Error => write!(f, "error"),
65 }
66 }
67}
68
69impl std::fmt::Display for Violation {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(
72 f,
73 "{}:{}:{}: {}/{}: {}",
74 self.line, self.column, self.severity, self.rule_id, self.rule_name, self.message
75 )
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_severity_display() {
85 assert_eq!(format!("{}", Severity::Info), "info");
86 assert_eq!(format!("{}", Severity::Warning), "warning");
87 assert_eq!(format!("{}", Severity::Error), "error");
88 }
89
90 #[test]
91 fn test_severity_ordering() {
92 assert!(Severity::Info < Severity::Warning);
93 assert!(Severity::Warning < Severity::Error);
94 assert!(Severity::Info < Severity::Error);
95 }
96
97 #[test]
98 fn test_violation_creation() {
99 let violation = Violation {
100 rule_id: "MD001".to_string(),
101 rule_name: "heading-increment".to_string(),
102 message: "Heading levels should only increment by one level at a time".to_string(),
103 line: 5,
104 column: 1,
105 severity: Severity::Warning,
106 fix: None,
107 };
108
109 assert_eq!(violation.rule_id, "MD001");
110 assert_eq!(violation.rule_name, "heading-increment");
111 assert_eq!(violation.line, 5);
112 assert_eq!(violation.column, 1);
113 assert_eq!(violation.severity, Severity::Warning);
114 assert_eq!(violation.fix, None);
115 }
116
117 #[test]
118 fn test_violation_display() {
119 let violation = Violation {
120 rule_id: "MD013".to_string(),
121 rule_name: "line-length".to_string(),
122 message: "Line too long".to_string(),
123 line: 10,
124 column: 81,
125 severity: Severity::Error,
126 fix: None,
127 };
128
129 let expected = "10:81:error: MD013/line-length: Line too long";
130 assert_eq!(format!("{violation}"), expected);
131 }
132
133 #[test]
134 fn test_violation_equality() {
135 let violation1 = Violation {
136 rule_id: "MD001".to_string(),
137 rule_name: "heading-increment".to_string(),
138 message: "Test message".to_string(),
139 line: 1,
140 column: 1,
141 severity: Severity::Warning,
142 fix: None,
143 };
144
145 let violation2 = Violation {
146 rule_id: "MD001".to_string(),
147 rule_name: "heading-increment".to_string(),
148 message: "Test message".to_string(),
149 line: 1,
150 column: 1,
151 severity: Severity::Warning,
152 fix: None,
153 };
154
155 let violation3 = Violation {
156 rule_id: "MD002".to_string(),
157 rule_name: "first-heading-h1".to_string(),
158 message: "Different message".to_string(),
159 line: 2,
160 column: 1,
161 severity: Severity::Error,
162 fix: None,
163 };
164
165 assert_eq!(violation1, violation2);
166 assert_ne!(violation1, violation3);
167 }
168
169 #[test]
170 fn test_violation_clone() {
171 let original = Violation {
172 rule_id: "MD040".to_string(),
173 rule_name: "fenced-code-language".to_string(),
174 message: "Fenced code blocks should have a language specified".to_string(),
175 line: 15,
176 column: 3,
177 severity: Severity::Info,
178 fix: None,
179 };
180
181 let cloned = original.clone();
182 assert_eq!(original, cloned);
183 }
184
185 #[test]
186 fn test_violation_debug() {
187 let violation = Violation {
188 rule_id: "MD025".to_string(),
189 rule_name: "single-h1".to_string(),
190 message: "Multiple top level headings in the same document".to_string(),
191 line: 20,
192 column: 1,
193 severity: Severity::Warning,
194 fix: None,
195 };
196
197 let debug_str = format!("{violation:?}");
198 assert!(debug_str.contains("MD025"));
199 assert!(debug_str.contains("single-h1"));
200 assert!(debug_str.contains("Multiple top level headings"));
201 assert!(debug_str.contains("line: 20"));
202 assert!(debug_str.contains("column: 1"));
203 assert!(debug_str.contains("Warning"));
204 }
205
206 #[test]
207 fn test_all_severity_variants() {
208 let severities = [Severity::Info, Severity::Warning, Severity::Error];
209
210 for severity in &severities {
211 let violation = Violation {
212 rule_id: "TEST".to_string(),
213 rule_name: "test-rule".to_string(),
214 message: "Test message".to_string(),
215 line: 1,
216 column: 1,
217 severity: *severity,
218 fix: None,
219 };
220
221 let display_str = format!("{violation}");
223 assert!(display_str.contains(&format!("{severity}")));
224 }
225 }
226
227 #[test]
228 fn test_violation_with_fix() {
229 let fix = Fix {
230 description: "Replace tab with spaces".to_string(),
231 replacement: Some(" ".to_string()),
232 start: Position {
233 line: 5,
234 column: 10,
235 },
236 end: Position {
237 line: 5,
238 column: 11,
239 },
240 };
241
242 let violation = Violation {
243 rule_id: "MD010".to_string(),
244 rule_name: "no-hard-tabs".to_string(),
245 message: "Hard tab found".to_string(),
246 line: 5,
247 column: 10,
248 severity: Severity::Warning,
249 fix: Some(fix.clone()),
250 };
251
252 assert_eq!(violation.fix, Some(fix));
253 assert!(violation.fix.is_some());
254
255 let fix_ref = violation.fix.as_ref().unwrap();
256 assert_eq!(fix_ref.description, "Replace tab with spaces");
257 assert_eq!(fix_ref.replacement, Some(" ".to_string()));
258 assert_eq!(fix_ref.start.line, 5);
259 assert_eq!(fix_ref.start.column, 10);
260 assert_eq!(fix_ref.end.line, 5);
261 assert_eq!(fix_ref.end.column, 11);
262 }
263
264 #[test]
265 fn test_fix_delete_operation() {
266 let fix = Fix {
267 description: "Remove extra newlines".to_string(),
268 replacement: None, start: Position {
270 line: 10,
271 column: 1,
272 },
273 end: Position {
274 line: 12,
275 column: 1,
276 },
277 };
278
279 assert_eq!(fix.replacement, None);
280 assert_eq!(fix.description, "Remove extra newlines");
281 }
282}