Skip to main content

max / balanced_breakfast

Clear the clippy backlog so BB passes its release gate Forty-two warnings across bb-core, bb-feed and the desktop crate, most of them collapsible_if that edition 2024 let-chains fold flat. BB was the only one of the eight Bento repos failing the prebuild gate. Two of clippy's suggestions were not taken as offered. json_to_i32 became i32::from(*b) rather than a guarded match arm that leans on fallthrough for the false case. The bookmark excerpt keeps its comment on the map() call instead of stranding it above a blank line. The rewrites that moved a condition into a match guard (atom summary/published, the query-feed operators) all fall through to a no-op arm when the guard fails, so they read differently but decide the same. 611 tests pass; clippy -D warnings is clean with the release features.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 04:29 UTC
Signed with PGP, not checked
Commit: 9638766ea23a5a844115a430457ec268d59e37fb
Parent: ffbd7ab
14 files changed, +242 insertions, -262 deletions
@@ -123,10 +123,10 @@
123 123
124 124 // Clean up old download temp files from previous sessions
125 125 let downloads_dir = std::env::temp_dir().join("bb-downloads");
126 - if downloads_dir.exists() {
127 - if let Err(e) = std::fs::remove_dir_all(&downloads_dir) {
128 - debug!(error = %e, "Failed to clean up old download temp files");
129 - }
126 + if downloads_dir.exists()
127 + && let Err(e) = std::fs::remove_dir_all(&downloads_dir)
128 + {
129 + debug!(error = %e, "Failed to clean up old download temp files");
130 130 }
131 131
132 132 info!("Application state initialized");
@@ -265,10 +265,10 @@
265 265 .parent()
266 266 .map(|p| p.join("plugins"));
267 267
268 - if let Some(ref dev_path) = dev_plugins {
269 - if dev_path.exists() {
270 - return dev_path.clone();
271 - }
268 + if let Some(ref dev_path) = dev_plugins
269 + && dev_path.exists()
270 + {
271 + return dev_path.clone();
272 272 }
273 273
274 274 // In production, use the app config dir (fall back to app data dir)
@@ -296,14 +296,12 @@
296 296 if path.extension().is_some_and(|e| e == "rhai") {
297 297 let dest = plugins_dir.join(entry.file_name());
298 298 // Skip if installed file is identical to bundled version
299 - if dest.exists() {
300 - if let (Ok(bundled_bytes), Ok(installed_bytes)) =
299 + if dest.exists()
300 + && let (Ok(bundled_bytes), Ok(installed_bytes)) =
301 301 (std::fs::read(&path), std::fs::read(&dest))
302 - {
303 - if bundled_bytes == installed_bytes {
304 - continue;
305 - }
306 - }
302 + && bundled_bytes == installed_bytes
303 + {
304 + continue;
307 305 }
308 306 if let Err(e) = std::fs::copy(&path, &dest) {
309 307 tracing::warn!(error = %e, ?path, "Failed to copy plugin");
@@ -133,23 +133,23 @@
133 133 }
134 134 };
135 135
136 - if !last_sync.is_empty() {
137 - if let Ok(last) = chrono::DateTime::parse_from_rfc3339(&last_sync) {
138 - let elapsed = chrono::Utc::now() - last.with_timezone(&chrono::Utc);
139 - if elapsed.num_minutes() < interval_minutes as i64 {
140 - continue;
141 - }
136 + if !last_sync.is_empty()
137 + && let Ok(last) = chrono::DateTime::parse_from_rfc3339(&last_sync)
138 + {
139 + let elapsed = chrono::Utc::now() - last.with_timezone(&chrono::Utc);
140 + if elapsed.num_minutes() < interval_minutes as i64 {
141 + continue;
142 142 }
143 143 }
144 144 }
145 145 sse_triggered = false;
146 146
147 147 // Check backoff
148 - if let Some(until) = backoff_until {
149 - if chrono::Utc::now() < until {
150 - debug!(%until, "Sync scheduler: backing off");
151 - continue;
152 - }
148 + if let Some(until) = backoff_until
149 + && chrono::Utc::now() < until
150 + {
151 + debug!(%until, "Sync scheduler: backing off");
152 + continue;
153 153 }
154 154
155 155 // Prevent concurrent sync operations (manual + scheduler).
@@ -104,7 +104,7 @@
104 104 if file_path.exists() {
105 105 let key = load_or_create_key(file_path)?;
106 106 // Migrate to keychain
107 - let b64 = BASE64.encode(&*key);
107 + let b64 = BASE64.encode(*key);
108 108 if entry.set_password(&b64).is_ok() {
109 109 // Read back and verify before deleting the file fallback
110 110 if let Ok(readback) = entry.get_password() {
@@ -138,7 +138,7 @@
138 138 and will be flagged for re-entry."
139 139 );
140 140 let key = generate_key();
141 - let b64 = BASE64.encode(&*key);
141 + let b64 = BASE64.encode(*key);
142 142 if entry.set_password(&b64).is_ok() {
143 143 return Ok(key);
144 144 }
@@ -213,15 +213,16 @@
213 213 if field.field_type != ConfigFieldType::Secret {
214 214 continue;
215 215 }
216 - if let Some(serde_json::Value::String(val)) = obj.get(&field.key) {
217 - if !val.is_empty() && !val.starts_with(PREFIX) {
218 - match encrypt_field(val, key) {
219 - Ok(encrypted) => {
220 - obj.insert(field.key.clone(), serde_json::Value::String(encrypted));
221 - }
222 - Err(e) => {
223 - tracing::warn!(field = %field.key, error = %e, "Failed to encrypt secret, plaintext retained");
224 - }
216 + if let Some(serde_json::Value::String(val)) = obj.get(&field.key)
217 + && !val.is_empty()
218 + && !val.starts_with(PREFIX)
219 + {
220 + match encrypt_field(val, key) {
221 + Ok(encrypted) => {
222 + obj.insert(field.key.clone(), serde_json::Value::String(encrypted));
223 + }
224 + Err(e) => {
225 + tracing::warn!(field = %field.key, error = %e, "Failed to encrypt secret, plaintext retained");
225 226 }
226 227 }
227 228 }
@@ -249,17 +250,17 @@
249 250 if field.field_type != ConfigFieldType::Secret {
250 251 continue;
251 252 }
252 - if let Some(serde_json::Value::String(val)) = obj.get(&field.key) {
253 - if val.starts_with(PREFIX) {
254 - match decrypt_field(val, key) {
255 - Ok(decrypted) => {
256 - obj.insert(field.key.clone(), serde_json::Value::String(decrypted));
257 - }
258 - Err(e) => {
259 - tracing::error!(field = %field.key, error = %e, "Failed to decrypt secret, clearing field to prevent ciphertext leakage. Prompting for re-entry.");
260 - obj.insert(field.key.clone(), serde_json::Value::String(String::new()));
261 - needs_reentry.push(field.key.clone());
262 - }
253 + if let Some(serde_json::Value::String(val)) = obj.get(&field.key)
254 + && val.starts_with(PREFIX)
255 + {
256 + match decrypt_field(val, key) {
257 + Ok(decrypted) => {
258 + obj.insert(field.key.clone(), serde_json::Value::String(decrypted));
259 + }
260 + Err(e) => {
261 + tracing::error!(field = %field.key, error = %e, "Failed to decrypt secret, clearing field to prevent ciphertext leakage. Prompting for re-entry.");
262 + obj.insert(field.key.clone(), serde_json::Value::String(String::new()));
263 + needs_reentry.push(field.key.clone());
263 264 }
264 265 }
265 266 }
@@ -40,7 +40,7 @@
40 40 pub fn apply(&self, items: &mut [FeedItem]) {
41 41 match self {
42 42 OrderBy::Chronological => {
43 - items.sort_by(|a, b| b.meta.published_at.cmp(&a.meta.published_at));
43 + items.sort_by_key(|i| std::cmp::Reverse(i.meta.published_at));
44 44 }
45 45 OrderBy::Score => {
46 46 // Use i64::MIN for None so scoreless items sort after all scored items
@@ -205,10 +205,10 @@
205 205 /// Check if an item matches using pre-compiled regex cache.
206 206 pub fn matches_with_cache(&self, item: &FeedItem, regex_cache: &HashMap<usize, Regex>) -> bool {
207 207 // Check source filter
208 - if let Some(ref source) = self.source {
209 - if item.id.source.as_str() != source {
210 - return false;
211 - }
208 + if let Some(ref source) = self.source
209 + && item.id.source.as_str() != source
210 + {
211 + return false;
212 212 }
213 213
214 214 // Check unread filter
@@ -266,26 +266,24 @@
266 266 _ => "",
267 267 };
268 268 match c.operator.as_str() {
269 - "contains" => {
270 - if !field_value.to_lowercase().contains(&c.value.to_lowercase()) {
271 - return false;
272 - }
269 + "contains"
270 + if !field_value.to_lowercase().contains(&c.value.to_lowercase()) =>
271 + {
272 + return false;
273 273 }
274 - "not_contains" => {
275 - if field_value.to_lowercase().contains(&c.value.to_lowercase()) {
276 - return false;
277 - }
274 + "not_contains"
275 + if field_value.to_lowercase().contains(&c.value.to_lowercase()) =>
276 + {
277 + return false;
278 278 }
279 - "equals" => {
280 - if !field_value.eq_ignore_ascii_case(&c.value) {
281 - return false;
282 - }
279 + "equals" if !field_value.eq_ignore_ascii_case(&c.value) => {
280 + return false;
283 281 }
284 282 "matches_regex" => {
285 - if let Some(re) = regex_cache.get(&i) {
286 - if !re.is_match(field_value) {
287 - return false;
288 - }
283 + if let Some(re) = regex_cache.get(&i)
284 + && !re.is_match(field_value)
285 + {
286 + return false;
289 287 }
290 288 // Missing from cache = invalid regex, skip (already warned)
291 289 }
@@ -213,10 +213,10 @@
213 213 {
214 214 return Err(ApiError::bad_request("Item is already bookmarked"));
215 215 }
216 - if let Some(ref url) = item.url {
217 - if db.bookmarks().get_by_url(url).await?.is_some() {
218 - return Err(ApiError::bad_request("URL is already bookmarked"));
219 - }
216 + if let Some(ref url) = item.url
217 + && db.bookmarks().get_by_url(url).await?.is_some()
218 + {
219 + return Err(ApiError::bad_request("URL is already bookmarked"));
220 220 }
221 221
222 222 let url = item.url.clone().unwrap_or_default();
@@ -232,11 +232,8 @@
232 232 description: item
233 233 .body
234 234 .clone()
235 - .map(|b| {
236 - // Truncate body to a description-length excerpt
237 - let plain = b.chars().take(300).collect::<String>();
238 - plain
239 - })
235 + // Truncate body to a description-length excerpt
236 + .map(|b| b.chars().take(300).collect::<String>())
240 237 .unwrap_or_default(),
241 238 author: item.bite_author.clone(),
242 239 source_name: item.source_name.clone(),
@@ -120,58 +120,58 @@
120 120 )));
121 121 }
122 122
123 - if let Some(serde_json::Value::String(s)) = value {
124 - if !s.is_empty() {
125 - match field.field_type {
126 - bb_interface::ConfigFieldType::Url => {
127 - if !s.starts_with("http://") && !s.starts_with("https://") {
128 - return Err(ApiError::bad_request(format!(
129 - "'{}' must start with http:// or https://",
130 - field.label
131 - )));
132 - }
133 - let after_scheme = s.split("://").nth(1).unwrap_or("");
134 - if after_scheme.is_empty() || after_scheme == "/" {
135 - return Err(ApiError::bad_request(format!(
136 - "'{}' is not a valid URL",
137 - field.label
138 - )));
139 - }
123 + if let Some(serde_json::Value::String(s)) = value
124 + && !s.is_empty()
125 + {
126 + match field.field_type {
127 + bb_interface::ConfigFieldType::Url => {
128 + if !s.starts_with("http://") && !s.starts_with("https://") {
129 + return Err(ApiError::bad_request(format!(
130 + "'{}' must start with http:// or https://",
131 + field.label
132 + )));
140 133 }
141 - bb_interface::ConfigFieldType::Number => {
142 - if s.parse::<f64>().is_err() {
143 - return Err(ApiError::bad_request(format!(
144 - "'{}' must be a valid number",
145 - field.label
146 - )));
147 - }
134 + let after_scheme = s.split("://").nth(1).unwrap_or("");
135 + if after_scheme.is_empty() || after_scheme == "/" {
136 + return Err(ApiError::bad_request(format!(
137 + "'{}' is not a valid URL",
138 + field.label
139 + )));
148 140 }
149 - bb_interface::ConfigFieldType::Select => {
150 - if !field.options.contains(s) {
151 - return Err(ApiError::bad_request(format!(
152 - "'{}' must be one of: {}",
153 - field.label,
154 - field.options.join(", ")
155 - )));
156 - }
141 + }
142 + bb_interface::ConfigFieldType::Number => {
143 + if s.parse::<f64>().is_err() {
144 + return Err(ApiError::bad_request(format!(
145 + "'{}' must be a valid number",
146 + field.label
147 + )));
157 148 }
158 - bb_interface::ConfigFieldType::Toggle => {
159 - if s != "true" && s != "false" {
160 - return Err(ApiError::bad_request(format!(
161 - "'{}' must be \"true\" or \"false\"",
162 - field.label
163 - )));
164 - }
149 + }
150 + bb_interface::ConfigFieldType::Select => {
151 + if !field.options.contains(s) {
152 + return Err(ApiError::bad_request(format!(
153 + "'{}' must be one of: {}",
154 + field.label,
155 + field.options.join(", ")
156 + )));
165 157 }
166 - bb_interface::ConfigFieldType::Text
167 - | bb_interface::ConfigFieldType::TextArea
168 - | bb_interface::ConfigFieldType::Secret => {
169 - if s.len() > 10_000 {
170 - return Err(ApiError::bad_request(format!(
171 - "'{}' exceeds maximum length of 10,000 characters",
172 - field.label
173 - )));
174 - }
158 + }
159 + bb_interface::ConfigFieldType::Toggle => {
160 + if s != "true" && s != "false" {
161 + return Err(ApiError::bad_request(format!(
162 + "'{}' must be \"true\" or \"false\"",
163 + field.label
164 + )));
165 + }
166 + }
167 + bb_interface::ConfigFieldType::Text
168 + | bb_interface::ConfigFieldType::TextArea
169 + | bb_interface::ConfigFieldType::Secret => {
170 + if s.len() > 10_000 {
171 + return Err(ApiError::bad_request(format!(
172 + "'{}' exceeds maximum length of 10,000 characters",
173 + field.label
174 + )));
175 175 }
176 176 }
177 177 }
@@ -434,7 +434,7 @@
434 434 ));
435 435 }
436 436
437 - let result = tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
437 + tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
438 438 let resp = ureq::get(&url)
439 439 .call()
440 440 .map_err(|e| ApiError::internal(format!("Download failed: {}", e)))?;
@@ -493,9 +493,7 @@
493 493 Ok(())
494 494 })
495 495 .await
496 - .map_err(|e| ApiError::internal(format!("Task join error: {}", e)))?;
497 -
498 - result
496 + .map_err(|e| ApiError::internal(format!("Task join error: {}", e)))?
499 497 }
500 498
501 499 #[cfg(test)]
@@ -148,10 +148,10 @@
148 148
149 149 // Re-initialize the RSS plugin so it discovers the newly imported feed
150 150 // URLs without requiring an app restart.
151 - if imported > 0 {
152 - if let Err(e) = state.orchestrator.init_plugin_from_db("rss").await {
153 - tracing::warn!(error = %e, "Failed to reinitialize RSS plugin");
154 - }
151 + if imported > 0
152 + && let Err(e) = state.orchestrator.init_plugin_from_db("rss").await
153 + {
154 + tracing::warn!(error = %e, "Failed to reinitialize RSS plugin");
155 155 }
156 156
157 157 info!(
@@ -112,6 +112,7 @@
112 112 /// Returns the port. The server handles:
113 113 /// - The browser redirect with `?code=...&state=...` (stores result, returns HTML)
114 114 /// - `/result` polling endpoint (returns JSON: pending, success, or error)
115 + ///
115 116 /// Any previously running callback server is cancelled via the shared generation counter.
116 117 fn start_callback_server() -> Result<u16, ApiError> {
117 118 let listener = std::net::TcpListener::bind("127.0.0.1:0")
@@ -101,10 +101,10 @@
101 101
102 102 for table in UPSERT_ORDER {
103 103 for change in &upserts {
104 - if change.table == *table {
105 - if let Some(ref data) = change.data {
106 - apply_upsert_exec(&mut **tx, table, &change.row_id, data).await?;
107 - }
104 + if change.table == *table
105 + && let Some(ref data) = change.data
106 + {
107 + apply_upsert_exec(tx, table, &change.row_id, data).await?;
108 108 }
109 109 }
110 110 }
@@ -112,7 +112,7 @@
112 112 for table in DELETE_ORDER {
113 113 for change in &deletes {
114 114 if change.table == *table {
115 - apply_delete_exec(&mut **tx, table, &change.row_id).await?;
115 + apply_delete_exec(tx, table, &change.row_id).await?;
116 116 }
117 117 }
118 118 }
@@ -179,7 +179,7 @@
179 179 data: &serde_json::Value,
180 180 ) -> Result<(), ApiError> {
181 181 let mut conn = pool.acquire().await.map_err(super::db_err)?;
182 - apply_upsert_exec(&mut *conn, table, row_id, data).await
182 + apply_upsert_exec(&mut conn, table, row_id, data).await
183 183 }
184 184
185 185 /// Apply an INSERT OR REPLACE for a remote change on any SQLite executor.
@@ -286,13 +286,7 @@
286 286 fn json_to_i32(val: &serde_json::Value) -> i32 {
287 287 match val {
288 288 serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i32,
289 - serde_json::Value::Bool(b) => {
290 - if *b {
291 - 1
292 - } else {
293 - 0
294 - }
295 - }
289 + serde_json::Value::Bool(b) => i32::from(*b),
296 290 _ => 0,
297 291 }
298 292 }
@@ -301,7 +295,7 @@
301 295 #[cfg(test)]
302 296 async fn apply_delete(pool: &SqlitePool, table: &str, row_id: &str) -> Result<(), ApiError> {
303 297 let mut conn = pool.acquire().await.map_err(super::db_err)?;
304 - apply_delete_exec(&mut *conn, table, row_id).await
298 + apply_delete_exec(&mut conn, table, row_id).await
305 299 }
306 300
307 301 /// Apply a DELETE for a remote change on any SQLite executor.