1use std::collections::BTreeMap;
2
3use crate::compress::Compressor;
4
5pub struct TscCompressor;
6
7impl Compressor for TscCompressor {
8 fn matches(&self, command: &str) -> bool {
9 command.split_whitespace().any(|token| token == "tsc")
10 }
11
12 fn compress(&self, _command: &str, output: &str) -> String {
13 compress_tsc(output)
14 }
15}
16
17fn compress_tsc(output: &str) -> String {
18 let lines: Vec<&str> = output.lines().collect();
19 let error_lines: Vec<&str> = lines
20 .iter()
21 .copied()
22 .filter(|line| is_tsc_error_line(line))
23 .collect();
24
25 if error_lines.is_empty() {
26 return "No errors. [cmpaft]".to_string();
27 }
28
29 let mut by_file: BTreeMap<String, Vec<String>> = BTreeMap::new();
30 let mut ungrouped = Vec::new();
31 for line in error_lines {
32 if let Some(file) = error_file(line) {
33 by_file.entry(file).or_default().push(line.to_string());
34 } else {
35 ungrouped.push(line.to_string());
36 }
37 }
38
39 let mut result = Vec::new();
40 let mut emitted_files = 0usize;
41 for errors in by_file.values() {
42 if emitted_files >= 10 && by_file.len() > 20 {
43 continue;
44 }
45 emitted_files += 1;
46 if errors.len() > 30 {
47 result.extend(errors.iter().take(10).cloned());
48 result.push(format!(
49 "... and {} more errors in this file",
50 errors.len() - 10
51 ));
52 } else {
53 result.extend(errors.iter().cloned());
54 }
55 }
56
57 result.extend(ungrouped);
58 if by_file.len() > 20 {
59 result.push(format!(
60 "... and {} more files with errors",
61 by_file.len() - emitted_files
62 ));
63 }
64 if let Some(summary) = lines.iter().rev().find(|line| is_tsc_summary(line)) {
65 result.push((*summary).to_string());
66 }
67
68 trim_trailing_lines(&result.join("\n"))
69}
70
71fn is_tsc_error_line(line: &str) -> bool {
72 line.contains(": error TS") && error_file(line).is_some()
73}
74
75fn error_file(line: &str) -> Option<String> {
76 let marker = line.find("): error TS")?;
77 let before = &line[..marker];
78 let open = before.rfind('(')?;
79 if before[open + 1..]
80 .split(',')
81 .all(|part| !part.is_empty() && part.chars().all(|char| char.is_ascii_digit()))
82 {
83 Some(before[..open].to_string())
84 } else {
85 None
86 }
87}
88
89fn is_tsc_summary(line: &str) -> bool {
90 let trimmed = line.trim();
91 trimmed.starts_with("Found ") && trimmed.contains(" errors") && trimmed.contains(" files")
92}
93
94fn trim_trailing_lines(input: &str) -> String {
95 input
96 .lines()
97 .map(str::trim_end)
98 .collect::<Vec<_>>()
99 .join("\n")
100}