Skip to main content

lean_ctx_patterns/
typescript.rs

1// SPDX-License-Identifier: Apache-2.0
2
3macro_rules! static_regex {
4    ($pattern:expr_2021) => {{
5        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
6        RE.get_or_init(|| {
7            regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
8        })
9    }};
10}
11
12/// `file(line,col): error TSnnnn: msg` (plain) or `file:line:col - error TSnnnn: msg` (`--pretty`).
13fn tsc_error_re() -> &'static regex::Regex {
14    static_regex!(r"^(?:(\S+)\((\d+),(\d+)\):|(\S+?):(\d+):(\d+) -) error (TS\d+): (.+)$")
15}
16fn error_count_re() -> &'static regex::Regex {
17    static_regex!(r"Found (\d+) errors?")
18}
19
20/// Groups diagnostics by file. The full path, `line:col`, error code and the
21/// complete message (plus tsc's indented elaboration lines) are kept — the
22/// message tail is what names the offending type, so it is never cut (#1894).
23pub fn compress(output: &str) -> Option<String> {
24    let mut files: Vec<(String, Vec<String>)> = Vec::new();
25    let mut total_errors = 0u32;
26    let mut diagnostics = 0u32;
27    let mut in_diagnostic = false;
28
29    for line in output.lines() {
30        if let Some(caps) = tsc_error_re().captures(line) {
31            let (file, line_no, col) = match caps.get(1) {
32                Some(f) => (f.as_str(), &caps[2], &caps[3]),
33                None => (&caps[4], &caps[5], &caps[6]),
34            };
35            let entry = format!("  {line_no}:{col} {} {}", &caps[7], caps[8].trim());
36            match files.iter_mut().find(|(f, _)| f == file) {
37                Some((_, list)) => list.push(entry),
38                None => files.push((file.to_string(), vec![entry])),
39            }
40            diagnostics += 1;
41            in_diagnostic = true;
42            continue;
43        }
44        if let Some(caps) = error_count_re().captures(line) {
45            total_errors = caps[1].parse().unwrap_or(0);
46            in_diagnostic = false;
47            continue;
48        }
49        let is_elaboration = line.starts_with("  ") && !line.trim().is_empty();
50        if in_diagnostic
51            && is_elaboration
52            && let Some((_, list)) = files.last_mut()
53        {
54            list.push(format!("    {}", line.trim()));
55        } else {
56            in_diagnostic = false;
57        }
58    }
59
60    if files.is_empty() {
61        return None;
62    }
63
64    let total = if total_errors > 0 {
65        total_errors
66    } else {
67        diagnostics
68    };
69    let mut result = vec![format!("{total} errors in {} files:", files.len())];
70    for (file, entries) in files {
71        result.push(file);
72        result.extend(entries);
73    }
74    Some(result.join("\n"))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::compress;
80
81    #[test]
82    fn keeps_full_path_position_and_whole_message() {
83        let out = "src/a/index.ts(3,7): error TS2322: Type '{ id: string; }' is not assignable to type 'UserRecordWithPermissions'.\n  Property 'permissions' is missing in type '{ id: string; }'.\nsrc/b/index.ts(10,1): error TS2304: Cannot find name 'foo'.\n\nFound 2 errors in 2 files.\n";
84        assert_eq!(
85            compress(out).unwrap(),
86            "2 errors in 2 files:\nsrc/a/index.ts\n  3:7 TS2322 Type '{ id: string; }' is not assignable to type 'UserRecordWithPermissions'.\n    Property 'permissions' is missing in type '{ id: string; }'.\nsrc/b/index.ts\n  10:1 TS2304 Cannot find name 'foo'."
87        );
88    }
89
90    #[test]
91    fn pretty_format_and_grouping() {
92        let out = "src/x.ts:1:2 - error TS1005: ';' expected.\n\n1 const a = 1 const b\n\nsrc/x.ts:4:1 - error TS2304: Cannot find name 'c'.\n\nFound 2 errors in the same file, starting at: src/x.ts:1\n";
93        assert_eq!(
94            compress(out).unwrap(),
95            "2 errors in 1 files:\nsrc/x.ts\n  1:2 TS1005 ';' expected.\n  4:1 TS2304 Cannot find name 'c'."
96        );
97    }
98
99    #[test]
100    fn no_diagnostics_is_not_handled() {
101        assert!(compress("Version 5.4.5\n").is_none());
102    }
103}