Skip to main content

lean_ctx/core/patterns/
flutter.rs

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