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