lean_ctx/core/patterns/
flutter.rs1use 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}