rumdl_lib/output/formatters/
pylint.rs1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5
6pub struct PylintFormatter;
8
9impl Default for PylintFormatter {
10 fn default() -> Self {
11 Self
12 }
13}
14
15impl PylintFormatter {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21impl OutputFormatter for PylintFormatter {
22 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
23 let mut output = String::new();
24
25 for warning in warnings {
26 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
27
28 let pylint_code = if let Some(stripped) = rule_name.strip_prefix("MD") {
31 format!("CMD{stripped}")
32 } else {
33 format!("C{rule_name}")
34 };
35
36 let line = format!(
38 "{}:{}:{}: [{}] {}",
39 file_path, warning.line, warning.column, pylint_code, warning.message
40 );
41
42 output.push_str(&line);
43 output.push('\n');
44 }
45
46 if output.ends_with('\n') {
48 output.pop();
49 }
50
51 output
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::rule::{Fix, Severity};
59
60 #[test]
61 fn test_pylint_formatter_default() {
62 let _formatter = PylintFormatter;
63 }
65
66 #[test]
67 fn test_pylint_formatter_new() {
68 let _formatter = PylintFormatter::new();
69 }
71
72 #[test]
73 fn test_format_warnings_empty() {
74 let formatter = PylintFormatter::new();
75 let warnings = vec![];
76 let output = formatter.format_warnings(&warnings, "test.md");
77 assert_eq!(output, "");
78 }
79
80 #[test]
81 fn test_format_single_warning() {
82 let formatter = PylintFormatter::new();
83 let warnings = vec![LintWarning {
84 line: 10,
85 column: 5,
86 end_line: 10,
87 end_column: 15,
88 rule_name: Some("MD001".to_string()),
89 message: "Heading levels should only increment by one level at a time".to_string(),
90 severity: Severity::Warning,
91 fix: None,
92 }];
93
94 let output = formatter.format_warnings(&warnings, "README.md");
95 assert_eq!(
96 output,
97 "README.md:10:5: [CMD001] Heading levels should only increment by one level at a time"
98 );
99 }
100
101 #[test]
102 fn test_format_multiple_warnings() {
103 let formatter = PylintFormatter::new();
104 let warnings = vec![
105 LintWarning {
106 line: 5,
107 column: 1,
108 end_line: 5,
109 end_column: 10,
110 rule_name: Some("MD001".to_string()),
111 message: "First warning".to_string(),
112 severity: Severity::Warning,
113 fix: None,
114 },
115 LintWarning {
116 line: 10,
117 column: 3,
118 end_line: 10,
119 end_column: 20,
120 rule_name: Some("MD013".to_string()),
121 message: "Second warning".to_string(),
122 severity: Severity::Error,
123 fix: None,
124 },
125 ];
126
127 let output = formatter.format_warnings(&warnings, "test.md");
128 let expected = "test.md:5:1: [CMD001] First warning\ntest.md:10:3: [CMD013] Second warning";
129 assert_eq!(output, expected);
130 }
131
132 #[test]
133 fn test_format_warning_with_fix() {
134 let formatter = PylintFormatter::new();
135 let warnings = vec![LintWarning {
136 line: 15,
137 column: 1,
138 end_line: 15,
139 end_column: 10,
140 rule_name: Some("MD022".to_string()),
141 message: "Headings should be surrounded by blank lines".to_string(),
142 severity: Severity::Warning,
143 fix: Some(Fix::new(100..110, "\n# Heading\n".to_string())),
144 }];
145
146 let output = formatter.format_warnings(&warnings, "doc.md");
147 assert_eq!(
149 output,
150 "doc.md:15:1: [CMD022] Headings should be surrounded by blank lines"
151 );
152 }
153
154 #[test]
155 fn test_format_warning_unknown_rule() {
156 let formatter = PylintFormatter::new();
157 let warnings = vec![LintWarning {
158 line: 1,
159 column: 1,
160 end_line: 1,
161 end_column: 5,
162 rule_name: None,
163 message: "Unknown rule warning".to_string(),
164 severity: Severity::Warning,
165 fix: None,
166 }];
167
168 let output = formatter.format_warnings(&warnings, "file.md");
169 assert_eq!(output, "file.md:1:1: [Cunknown] Unknown rule warning");
170 }
171
172 #[test]
173 fn test_format_warning_non_md_rule() {
174 let formatter = PylintFormatter::new();
175 let warnings = vec![LintWarning {
176 line: 1,
177 column: 1,
178 end_line: 1,
179 end_column: 5,
180 rule_name: Some("CUSTOM001".to_string()),
181 message: "Custom rule warning".to_string(),
182 severity: Severity::Warning,
183 fix: None,
184 }];
185
186 let output = formatter.format_warnings(&warnings, "file.md");
187 assert_eq!(output, "file.md:1:1: [CCUSTOM001] Custom rule warning");
188 }
189
190 #[test]
191 fn test_pylint_code_conversion() {
192 let formatter = PylintFormatter::new();
193
194 let test_cases = vec![("MD001", "CMD001"), ("MD010", "CMD010"), ("MD999", "CMD999")];
196
197 for (md_code, expected_pylint) in test_cases {
198 let warnings = vec![LintWarning {
199 line: 1,
200 column: 1,
201 end_line: 1,
202 end_column: 1,
203 rule_name: Some(md_code.to_string()),
204 message: "Test".to_string(),
205 severity: Severity::Warning,
206 fix: None,
207 }];
208
209 let output = formatter.format_warnings(&warnings, "test.md");
210 assert!(output.contains(&format!("[{expected_pylint}]")));
211 }
212 }
213
214 #[test]
215 fn test_edge_cases() {
216 let formatter = PylintFormatter::new();
217
218 let warnings = vec![LintWarning {
220 line: 99999,
221 column: 12345,
222 end_line: 100000,
223 end_column: 12350,
224 rule_name: Some("MD999".to_string()),
225 message: "Edge case warning".to_string(),
226 severity: Severity::Error,
227 fix: None,
228 }];
229
230 let output = formatter.format_warnings(&warnings, "large.md");
231 assert_eq!(output, "large.md:99999:12345: [CMD999] Edge case warning");
232 }
233
234 #[test]
235 fn test_special_characters_in_message() {
236 let formatter = PylintFormatter::new();
237 let warnings = vec![LintWarning {
238 line: 1,
239 column: 1,
240 end_line: 1,
241 end_column: 5,
242 rule_name: Some("MD001".to_string()),
243 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
244 severity: Severity::Warning,
245 fix: None,
246 }];
247
248 let output = formatter.format_warnings(&warnings, "test.md");
249 assert_eq!(
250 output,
251 "test.md:1:1: [CMD001] Warning with \"quotes\" and 'apostrophes' and \n newline"
252 );
253 }
254
255 #[test]
256 fn test_special_characters_in_file_path() {
257 let formatter = PylintFormatter::new();
258 let warnings = vec![LintWarning {
259 line: 1,
260 column: 1,
261 end_line: 1,
262 end_column: 5,
263 rule_name: Some("MD001".to_string()),
264 message: "Test".to_string(),
265 severity: Severity::Warning,
266 fix: None,
267 }];
268
269 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
270 assert_eq!(output, "path/with spaces/and-dashes.md:1:1: [CMD001] Test");
271 }
272
273 #[test]
274 fn test_severity_ignored() {
275 let formatter = PylintFormatter::new();
276
277 let warnings = vec![
279 LintWarning {
280 line: 1,
281 column: 1,
282 end_line: 1,
283 end_column: 5,
284 rule_name: Some("MD001".to_string()),
285 message: "Warning severity".to_string(),
286 severity: Severity::Warning,
287 fix: None,
288 },
289 LintWarning {
290 line: 2,
291 column: 1,
292 end_line: 2,
293 end_column: 5,
294 rule_name: Some("MD002".to_string()),
295 message: "Error severity".to_string(),
296 severity: Severity::Error,
297 fix: None,
298 },
299 ];
300
301 let output = formatter.format_warnings(&warnings, "test.md");
302 let lines: Vec<&str> = output.lines().collect();
303
304 assert!(lines[0].starts_with("test.md:1:1: [CMD001]"));
306 assert!(lines[1].starts_with("test.md:2:1: [CMD002]"));
307 }
308}