1use std::collections::BTreeMap;
2
3use colored::Colorize;
4
5use crate::analyzer::ParseError;
6use crate::cli::OutputFormat;
7use crate::file_traverser::TraversalResult;
8use crate::offense::OffenseKind;
9
10pub fn print_results(result: &TraversalResult, format: &OutputFormat) {
12 match format {
13 OutputFormat::File => print_results_by_file(result),
14 OutputFormat::Rule => print_results_by_rule(result),
15 OutputFormat::Plain => print_results_plain(result),
16 }
17
18 if !result.parse_errors.is_empty() {
19 print_parse_errors(&result.parse_errors);
20 }
21
22 print_statistics(result);
23}
24
25fn print_results_by_file(result: &TraversalResult) {
33 for analysis in &result.results {
34 if analysis.offenses.is_empty() {
35 continue;
36 }
37 println!("{}", analysis.path.bold());
38 for offense in &analysis.offenses {
39 println!(
40 " {} {}",
41 format!("L{}", offense.line).cyan(),
42 offense.kind.config_key()
43 );
44 }
45 println!();
46 }
47}
48
49fn print_results_by_rule(result: &TraversalResult) {
57 let mut grouped: BTreeMap<OffenseKind, Vec<(String, usize)>> = BTreeMap::new();
58
59 for analysis in &result.results {
60 for offense in &analysis.offenses {
61 grouped
62 .entry(offense.kind)
63 .or_default()
64 .push((analysis.path.clone(), offense.line));
65 }
66 }
67
68 for (kind, locations) in &grouped {
69 let count = locations.len();
70 println!(
71 "{} ({} {})",
72 kind.explanation().yellow(),
73 count,
74 pluralize("offense", count)
75 );
76 for (path, line) in locations {
77 println!(" {}:{}", path, line);
78 }
79 println!();
80 }
81}
82
83fn print_results_plain(result: &TraversalResult) {
89 for analysis in &result.results {
90 if analysis.offenses.is_empty() {
91 continue;
92 }
93 for offense in &analysis.offenses {
94 let location = format!("{}:{}", analysis.path, offense.line);
95 println!("{} {}.", location.red(), offense.kind.explanation());
96 }
97 println!();
98 }
99}
100
101fn print_parse_errors(errors: &[ParseError]) {
102 println!(
103 "rubyfast was unable to process some files because the\n\
104 internal parser is not able to read some characters or\n\
105 has timed out. Unprocessable files were:"
106 );
107 println!("-----------------------------------------------------");
108 for err in errors {
109 println!("{} - {}", err.path, err.message);
110 }
111 println!();
112}
113
114fn print_statistics(result: &TraversalResult) {
115 let files = result.files_inspected;
116 let offenses = result.total_offenses();
117 let parse_errors = result.parse_errors.len();
118
119 let files_str = format!("{} {} inspected", files, pluralize("file", files));
120
121 let offenses_str = format!("{} {} detected", offenses, pluralize("offense", offenses));
122
123 let colored_offenses = if offenses == 0 {
124 offenses_str.green().to_string()
125 } else {
126 offenses_str.red().to_string()
127 };
128
129 if parse_errors > 0 {
130 let errors_str = format!(
131 "{} unparsable {} found",
132 parse_errors,
133 pluralize("file", parse_errors)
134 );
135 println!(
136 "{}, {}, {}",
137 files_str.green(),
138 colored_offenses,
139 errors_str.red()
140 );
141 } else {
142 println!("{}, {}", files_str.green(), colored_offenses);
143 }
144}
145
146pub fn print_fix_results(
148 result: &TraversalResult,
149 total_fixed: usize,
150 total_errors: usize,
151 format: &OutputFormat,
152) {
153 let unfixable_result = filter_unfixable(result);
155 match format {
156 OutputFormat::File => print_results_by_file(&unfixable_result),
157 OutputFormat::Rule => print_results_by_rule(&unfixable_result),
158 OutputFormat::Plain => print_results_plain(&unfixable_result),
159 }
160
161 if !result.parse_errors.is_empty() {
162 print_parse_errors(&result.parse_errors);
163 }
164
165 print_fix_statistics(result, total_fixed, total_errors);
166}
167
168fn filter_unfixable(result: &TraversalResult) -> TraversalResult {
170 use crate::analyzer::AnalysisResult;
171
172 let results = result
173 .results
174 .iter()
175 .map(|analysis| {
176 let offenses = analysis
177 .offenses
178 .iter()
179 .filter(|o| o.fix.is_none())
180 .cloned()
181 .collect();
182 AnalysisResult {
183 path: analysis.path.clone(),
184 offenses,
185 }
186 })
187 .collect();
188
189 TraversalResult {
190 results,
191 parse_errors: vec![],
192 files_inspected: result.files_inspected,
193 }
194}
195
196fn print_fix_statistics(result: &TraversalResult, total_fixed: usize, total_errors: usize) {
197 let files = result.files_inspected;
198 let offenses = result.total_offenses();
199 let fixable: usize = result
200 .results
201 .iter()
202 .flat_map(|r| &r.offenses)
203 .filter(|o| o.fix.is_some())
204 .count();
205
206 let files_str = format!("{} {} inspected", files, pluralize("file", files));
207 let offenses_str = format!("{} {} detected", offenses, pluralize("offense", offenses));
208 let fixed_str = format!(
209 "{} {} fixed",
210 total_fixed,
211 pluralize("offense", total_fixed)
212 );
213
214 let colored_offenses = if offenses == 0 {
215 offenses_str.green().to_string()
216 } else {
217 offenses_str.red().to_string()
218 };
219
220 let colored_fixed = if total_fixed > 0 {
221 fixed_str.green().to_string()
222 } else {
223 fixed_str.to_string()
224 };
225
226 let unfixable = offenses.saturating_sub(fixable);
227 if total_errors > 0 {
228 let err_str = format!(
229 "{} {} skipped (syntax error after fix)",
230 total_errors,
231 pluralize("file", total_errors)
232 );
233 println!(
234 "{}, {}, {}, {}",
235 files_str.green(),
236 colored_offenses,
237 colored_fixed,
238 err_str.yellow()
239 );
240 } else if unfixable > 0 {
241 let unfixable_str = format!(
242 "{} {} cannot be auto-fixed",
243 unfixable,
244 pluralize("offense", unfixable)
245 );
246 println!(
247 "{}, {}, {}, {}",
248 files_str.green(),
249 colored_offenses,
250 colored_fixed,
251 unfixable_str.yellow()
252 );
253 } else {
254 println!(
255 "{}, {}, {}",
256 files_str.green(),
257 colored_offenses,
258 colored_fixed
259 );
260 }
261}
262
263fn pluralize(word: &str, count: usize) -> String {
264 if count == 1 {
265 word.to_string()
266 } else {
267 format!("{}s", word)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::analyzer::AnalysisResult;
275 use crate::fix::Fix;
276 use crate::offense::{Offense, OffenseKind};
277
278 #[test]
279 fn pluralize_singular() {
280 assert_eq!(pluralize("file", 1), "file");
281 }
282
283 #[test]
284 fn pluralize_plural() {
285 assert_eq!(pluralize("file", 0), "files");
286 assert_eq!(pluralize("offense", 2), "offenses");
287 }
288
289 fn make_result(offenses: Vec<Offense>) -> TraversalResult {
290 TraversalResult {
291 results: vec![AnalysisResult {
292 path: "test.rb".to_string(),
293 offenses,
294 }],
295 parse_errors: vec![],
296 files_inspected: 1,
297 }
298 }
299
300 #[test]
301 fn filter_unfixable_keeps_only_no_fix() {
302 let offenses = vec![
303 Offense::new(OffenseKind::GsubVsTr, 1),
304 Offense::with_fix(OffenseKind::ForLoopVsEach, 2, Fix::single(0, 3, "x")),
305 Offense::new(OffenseKind::SortVsSortBy, 3),
306 ];
307 let result = make_result(offenses);
308 let filtered = filter_unfixable(&result);
309 assert_eq!(filtered.results[0].offenses.len(), 2);
310 assert!(filtered.results[0].offenses.iter().all(|o| o.fix.is_none()));
311 }
312
313 #[test]
314 fn filter_unfixable_empty_when_all_fixable() {
315 let offenses = vec![Offense::with_fix(
316 OffenseKind::ForLoopVsEach,
317 1,
318 Fix::single(0, 3, "x"),
319 )];
320 let result = make_result(offenses);
321 let filtered = filter_unfixable(&result);
322 assert_eq!(filtered.results[0].offenses.len(), 0);
323 }
324
325 #[test]
326 fn print_results_by_file_no_panic() {
327 let result = make_result(vec![Offense::new(OffenseKind::GsubVsTr, 5)]);
328 print_results_by_file(&result);
329 }
330
331 #[test]
332 fn print_results_by_file_empty_no_panic() {
333 let result = make_result(vec![]);
334 print_results_by_file(&result);
335 }
336
337 #[test]
338 fn print_results_by_rule_no_panic() {
339 let result = make_result(vec![
340 Offense::new(OffenseKind::GsubVsTr, 5),
341 Offense::new(OffenseKind::GsubVsTr, 10),
342 ]);
343 print_results_by_rule(&result);
344 }
345
346 #[test]
347 fn print_results_plain_no_panic() {
348 let result = make_result(vec![Offense::new(OffenseKind::GsubVsTr, 5)]);
349 print_results_plain(&result);
350 }
351
352 #[test]
353 fn print_results_plain_empty_no_panic() {
354 let result = make_result(vec![]);
355 print_results_plain(&result);
356 }
357
358 #[test]
359 fn print_statistics_no_offenses() {
360 let result = make_result(vec![]);
361 print_statistics(&result);
362 }
363
364 #[test]
365 fn print_statistics_with_offenses() {
366 let result = make_result(vec![Offense::new(OffenseKind::GsubVsTr, 5)]);
367 print_statistics(&result);
368 }
369
370 #[test]
371 fn print_statistics_with_parse_errors() {
372 let result = TraversalResult {
373 results: vec![],
374 parse_errors: vec![ParseError {
375 path: "bad.rb".to_string(),
376 message: "syntax error".to_string(),
377 }],
378 files_inspected: 1,
379 };
380 print_statistics(&result);
381 }
382
383 #[test]
384 fn print_parse_errors_no_panic() {
385 let errors = vec![ParseError {
386 path: "bad.rb".to_string(),
387 message: "oops".to_string(),
388 }];
389 print_parse_errors(&errors);
390 }
391
392 #[test]
393 fn print_results_dispatches_all_formats() {
394 let result = make_result(vec![Offense::new(OffenseKind::GsubVsTr, 1)]);
395 print_results(&result, &OutputFormat::File);
396 print_results(&result, &OutputFormat::Rule);
397 print_results(&result, &OutputFormat::Plain);
398 }
399
400 #[test]
401 fn print_fix_results_no_panic() {
402 let offenses = vec![
403 Offense::new(OffenseKind::GsubVsTr, 1),
404 Offense::with_fix(OffenseKind::ForLoopVsEach, 2, Fix::single(0, 3, "x")),
405 ];
406 let result = make_result(offenses);
407 print_fix_results(&result, 1, 0, &OutputFormat::File);
408 }
409
410 #[test]
411 fn print_fix_results_with_errors() {
412 let offenses = vec![Offense::with_fix(
413 OffenseKind::ForLoopVsEach,
414 1,
415 Fix::single(0, 3, "x"),
416 )];
417 let result = make_result(offenses);
418 print_fix_results(&result, 0, 1, &OutputFormat::File);
419 }
420
421 #[test]
422 fn print_fix_results_all_fixed() {
423 let offenses = vec![Offense::with_fix(
424 OffenseKind::ForLoopVsEach,
425 1,
426 Fix::single(0, 3, "x"),
427 )];
428 let result = make_result(offenses);
429 print_fix_results(&result, 1, 0, &OutputFormat::File);
430 }
431
432 #[test]
433 fn print_fix_results_unfixable_remaining() {
434 let offenses = vec![
435 Offense::new(OffenseKind::GsubVsTr, 1),
436 Offense::with_fix(OffenseKind::ForLoopVsEach, 2, Fix::single(0, 3, "x")),
437 ];
438 let result = make_result(offenses);
439 print_fix_results(&result, 1, 0, &OutputFormat::Rule);
440 print_fix_results(&result, 1, 0, &OutputFormat::Plain);
441 }
442}