lean_ctx_patterns/
flutter.rs1macro_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}