Skip to main content

lean_ctx_patterns/
flutter.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
12fn built_artifact_re() -> &'static regex::Regex {
13    static_regex!(r"^✓\s+Built\s+(.+?)(?:\s+\(([^)]+)\))?\s*\.?\s*$")
14}
15fn flutter_test_summary_re() -> &'static regex::Regex {
16    static_regex!(r"^(\d{2}:\d{2}\s+\+\d+(?:\s+-\d+)?:\s+.+)$")
17}
18fn analyze_issues_re() -> &'static regex::Regex {
19    static_regex!(r"(?i)(?:^Analyzing\s+.+\.\.\.\s*)?(\d+)\s+issues?\s+found")
20}
21fn analyze_issue_line_re() -> &'static regex::Regex {
22    static_regex!(r"^\s*(error|warning|info)\s+•")
23}
24fn is_flutter_build_noise(line: &str) -> bool {
25    let t = line.trim();
26    let tl = t.to_ascii_lowercase();
27    tl.starts_with("running gradle task")
28        || tl.starts_with("running pod install")
29        || tl.starts_with("building with sound null safety")
30        || tl.starts_with("flutter build")
31        || tl.contains("resolving dependencies")
32        || (tl.contains("..") && tl.contains("ms") && tl.matches('%').count() >= 1)
33        || tl.starts_with("warning: the flutter tool")
34}
35
36pub fn compress(command: &str, output: &str) -> Option<String> {
37    let cl = command.trim().to_ascii_lowercase();
38    if cl.starts_with("flutter ") {
39        let sub = cl.split_whitespace().nth(1).unwrap_or("");
40        return match sub {
41            "build" => Some(compress_flutter_build(output)),
42            "test" => Some(compress_flutter_test(output)),
43            "analyze" => Some(compress_analyze(output)),
44            _ => None,
45        };
46    }
47    if cl.starts_with("dart ") && cl.split_whitespace().nth(1) == Some("analyze") {
48        return Some(compress_analyze(output));
49    }
50    None
51}
52
53fn compress_flutter_build(output: &str) -> String {
54    let mut parts = Vec::new();
55
56    for line in output.lines() {
57        let t = line.trim_end();
58        if t.trim().is_empty() {
59            continue;
60        }
61        if is_flutter_build_noise(t) {
62            continue;
63        }
64        let trim = t.trim();
65        if let Some(caps) = built_artifact_re().captures(trim) {
66            let path = caps[1].trim();
67            let size = caps.get(2).map_or("", |m| m.as_str());
68            if size.is_empty() {
69                parts.push(format!("Built {path}"));
70            } else {
71                parts.push(format!("Built {path} ({size})"));
72            }
73            continue;
74        }
75        let tl = trim.to_ascii_lowercase();
76        if tl.starts_with("error") || tl.contains(" error:") || tl.contains("compilation failed") {
77            parts.push(trim.to_string());
78        }
79        if tl.starts_with("fail") && tl.contains("build") {
80            parts.push(trim.to_string());
81        }
82    }
83
84    if parts.is_empty() {
85        compact_tail(output, 15)
86    } else {
87        parts.join("\n")
88    }
89}
90
91fn compress_flutter_test(output: &str) -> String {
92    let mut parts = Vec::new();
93    let mut failures = Vec::new();
94
95    for line in output.lines() {
96        let trim = line.trim();
97        if trim.is_empty() {
98            continue;
99        }
100        if flutter_test_summary_re().is_match(trim) {
101            parts.push(trim.to_string());
102            continue;
103        }
104        let tl = trim.to_ascii_lowercase();
105        if tl.contains("some tests failed")
106            || tl == "failed."
107            || tl.starts_with("test failed")
108            || tl.contains("exception:") && tl.contains("test")
109        {
110            parts.push(trim.to_string());
111        }
112        if trim.starts_with("Expected:") || trim.starts_with("Actual:") {
113            failures.push(trim.to_string());
114        }
115        if tl.contains("error:") && (tl.contains("test") || tl.contains("failed")) {
116            parts.push(trim.to_string());
117        }
118    }
119
120    if !failures.is_empty() {
121        parts.push("assertion detail:".to_string());
122        parts.extend(failures.into_iter().take(12).map(|l| format!("  {l}")));
123    }
124
125    if parts.is_empty() {
126        compact_tail(output, 20)
127    } else {
128        parts.join("\n")
129    }
130}
131
132fn compress_analyze(output: &str) -> String {
133    let mut parts = Vec::new();
134    let mut issues = Vec::new();
135    let mut saw_header = false;
136
137    for line in output.lines() {
138        let trim = line.trim_end();
139        if trim.trim().is_empty() {
140            continue;
141        }
142        let t = trim.trim();
143        let tl = t.to_ascii_lowercase();
144
145        if tl.starts_with("analyzing ") {
146            saw_header = true;
147            parts.push(t.to_string());
148            continue;
149        }
150        if tl.contains("no issues found") {
151            parts.push(t.to_string());
152            continue;
153        }
154        if let Some(caps) = analyze_issues_re().captures(t) {
155            parts.push(format!("{} issues found", &caps[1]));
156            continue;
157        }
158        if analyze_issue_line_re().is_match(t)
159            || tl.starts_with("  error •")
160            || tl.starts_with("  warning •")
161            || tl.starts_with("  info •")
162        {
163            issues.push(t.to_string());
164        }
165    }
166
167    if !issues.is_empty() {
168        parts.push(format!("{} issue line(s):", issues.len()));
169        for i in issues.into_iter().take(40) {
170            parts.push(format!("  {i}"));
171        }
172    }
173
174    if parts.is_empty() && saw_header {
175        return "analyze (no summary matched)".to_string();
176    }
177    if parts.is_empty() {
178        compact_tail(output, 25)
179    } else {
180        parts.join("\n")
181    }
182}
183
184fn compact_tail(output: &str, max: usize) -> String {
185    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
186    if lines.is_empty() {
187        return "ok".to_string();
188    }
189    if lines.len() <= max {
190        return lines.join("\n");
191    }
192    let start = lines.len().saturating_sub(max);
193    format!(
194        "... ({} earlier lines)\n{}",
195        start,
196        lines[start..].join("\n")
197    )
198}