1use std::collections::BTreeMap;
2
3use crate::compress::{generic::GenericCompressor, 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) || is_tsc_top_level_error_line(line))
23 .collect();
24
25 if error_lines.is_empty() {
26 if output_is_likely_success(output) {
27 return "No errors. [cmpaft]".to_string();
28 }
29
30 return GenericCompressor::compress_output(output);
31 }
32
33 let mut by_file: BTreeMap<String, Vec<String>> = BTreeMap::new();
34 let mut ungrouped = Vec::new();
35 for line in error_lines {
36 if let Some(file) = error_file(line) {
37 by_file.entry(file).or_default().push(line.to_string());
38 } else {
39 ungrouped.push(line.to_string());
40 }
41 }
42
43 let mut result = Vec::new();
44 let mut emitted_files = 0usize;
45 for errors in by_file.values() {
46 if emitted_files >= 10 && by_file.len() > 20 {
47 continue;
48 }
49 emitted_files += 1;
50 if errors.len() > 30 {
51 result.extend(errors.iter().take(10).cloned());
52 result.push(format!(
53 "... and {} more errors in this file",
54 errors.len() - 10
55 ));
56 } else {
57 result.extend(errors.iter().cloned());
58 }
59 }
60
61 result.extend(ungrouped);
62 if by_file.len() > 20 {
63 result.push(format!(
64 "... and {} more files with errors",
65 by_file.len() - emitted_files
66 ));
67 }
68 if let Some(summary) = lines.iter().rev().find(|line| is_tsc_summary(line)) {
69 result.push((*summary).to_string());
70 }
71
72 trim_trailing_lines(&result.join("\n"))
73}
74
75fn is_tsc_error_line(line: &str) -> bool {
76 line.contains(": error TS") && error_file(line).is_some()
77}
78
79fn is_tsc_top_level_error_line(line: &str) -> bool {
80 let trimmed = line.trim_start();
81 trimmed.starts_with("error TS")
82 && trimmed["error TS".len()..]
83 .chars()
84 .next()
85 .is_some_and(|char| char.is_ascii_digit())
86}
87
88fn output_is_likely_success(output: &str) -> bool {
89 let trimmed = output.trim();
90 trimmed.is_empty()
91 || trimmed
92 .lines()
93 .any(|line| line.trim().contains("Found 0 errors"))
94}
95
96fn error_file(line: &str) -> Option<String> {
97 let marker = line.find("): error TS")?;
98 let before = &line[..marker];
99 let open = before.rfind('(')?;
100 if before[open + 1..]
101 .split(',')
102 .all(|part| !part.is_empty() && part.chars().all(|char| char.is_ascii_digit()))
103 {
104 Some(before[..open].to_string())
105 } else {
106 None
107 }
108}
109
110fn is_tsc_summary(line: &str) -> bool {
111 let trimmed = line.trim();
112 trimmed.starts_with("Found ") && trimmed.contains(" errors") && trimmed.contains(" files")
113}
114
115fn trim_trailing_lines(input: &str) -> String {
116 input
117 .lines()
118 .map(str::trim_end)
119 .collect::<Vec<_>>()
120 .join("\n")
121}