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.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"),
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"),
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"),
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"),
141 message: "Headings should be surrounded by blank lines".to_string(),
142 severity: Severity::Warning,
143 fix: Some(Fix {
144 range: 100..110,
145 replacement: "\n# Heading\n".to_string(),
146 }),
147 }];
148
149 let output = formatter.format_warnings(&warnings, "doc.md");
150 assert_eq!(
152 output,
153 "doc.md:15:1: [CMD022] Headings should be surrounded by blank lines"
154 );
155 }
156
157 #[test]
158 fn test_format_warning_unknown_rule() {
159 let formatter = PylintFormatter::new();
160 let warnings = vec![LintWarning {
161 line: 1,
162 column: 1,
163 end_line: 1,
164 end_column: 5,
165 rule_name: None,
166 message: "Unknown rule warning".to_string(),
167 severity: Severity::Warning,
168 fix: None,
169 }];
170
171 let output = formatter.format_warnings(&warnings, "file.md");
172 assert_eq!(output, "file.md:1:1: [Cunknown] Unknown rule warning");
173 }
174
175 #[test]
176 fn test_format_warning_non_md_rule() {
177 let formatter = PylintFormatter::new();
178 let warnings = vec![LintWarning {
179 line: 1,
180 column: 1,
181 end_line: 1,
182 end_column: 5,
183 rule_name: Some("CUSTOM001"),
184 message: "Custom rule warning".to_string(),
185 severity: Severity::Warning,
186 fix: None,
187 }];
188
189 let output = formatter.format_warnings(&warnings, "file.md");
190 assert_eq!(output, "file.md:1:1: [CCUSTOM001] Custom rule warning");
191 }
192
193 #[test]
194 fn test_pylint_code_conversion() {
195 let formatter = PylintFormatter::new();
196
197 let test_cases = vec![("MD001", "CMD001"), ("MD010", "CMD010"), ("MD999", "CMD999")];
199
200 for (md_code, expected_pylint) in test_cases {
201 let warnings = vec![LintWarning {
202 line: 1,
203 column: 1,
204 end_line: 1,
205 end_column: 1,
206 rule_name: Some(md_code),
207 message: "Test".to_string(),
208 severity: Severity::Warning,
209 fix: None,
210 }];
211
212 let output = formatter.format_warnings(&warnings, "test.md");
213 assert!(output.contains(&format!("[{expected_pylint}]")));
214 }
215 }
216
217 #[test]
218 fn test_edge_cases() {
219 let formatter = PylintFormatter::new();
220
221 let warnings = vec![LintWarning {
223 line: 99999,
224 column: 12345,
225 end_line: 100000,
226 end_column: 12350,
227 rule_name: Some("MD999"),
228 message: "Edge case warning".to_string(),
229 severity: Severity::Error,
230 fix: None,
231 }];
232
233 let output = formatter.format_warnings(&warnings, "large.md");
234 assert_eq!(output, "large.md:99999:12345: [CMD999] Edge case warning");
235 }
236
237 #[test]
238 fn test_special_characters_in_message() {
239 let formatter = PylintFormatter::new();
240 let warnings = vec![LintWarning {
241 line: 1,
242 column: 1,
243 end_line: 1,
244 end_column: 5,
245 rule_name: Some("MD001"),
246 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
247 severity: Severity::Warning,
248 fix: None,
249 }];
250
251 let output = formatter.format_warnings(&warnings, "test.md");
252 assert_eq!(
253 output,
254 "test.md:1:1: [CMD001] Warning with \"quotes\" and 'apostrophes' and \n newline"
255 );
256 }
257
258 #[test]
259 fn test_special_characters_in_file_path() {
260 let formatter = PylintFormatter::new();
261 let warnings = vec![LintWarning {
262 line: 1,
263 column: 1,
264 end_line: 1,
265 end_column: 5,
266 rule_name: Some("MD001"),
267 message: "Test".to_string(),
268 severity: Severity::Warning,
269 fix: None,
270 }];
271
272 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
273 assert_eq!(output, "path/with spaces/and-dashes.md:1:1: [CMD001] Test");
274 }
275
276 #[test]
277 fn test_severity_ignored() {
278 let formatter = PylintFormatter::new();
279
280 let warnings = vec![
282 LintWarning {
283 line: 1,
284 column: 1,
285 end_line: 1,
286 end_column: 5,
287 rule_name: Some("MD001"),
288 message: "Warning severity".to_string(),
289 severity: Severity::Warning,
290 fix: None,
291 },
292 LintWarning {
293 line: 2,
294 column: 1,
295 end_line: 2,
296 end_column: 5,
297 rule_name: Some("MD002"),
298 message: "Error severity".to_string(),
299 severity: Severity::Error,
300 fix: None,
301 },
302 ];
303
304 let output = formatter.format_warnings(&warnings, "test.md");
305 let lines: Vec<&str> = output.lines().collect();
306
307 assert!(lines[0].starts_with("test.md:1:1: [CMD001]"));
309 assert!(lines[1].starts_with("test.md:2:1: [CMD002]"));
310 }
311}