Skip to main content

aft/compress/
tsc.rs

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