Skip to main content

lean_ctx/core/patterns/
flutter.rs

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