From edccc14470537160113d145de6d00db0fc548d5b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 14:42:29 -0400 Subject: [PATCH] Never crash: UI and app shells (ui-egui, vectorcraft, vectorcraft-web, cli perf) Removes the remaining production unwraps in the UI (fit view, save, macOS shortcut labels, layers rows, swatch rows) and the CLI perf budget runner (errors are reported instead of expect), and adds the deny attribute to ui-egui, the desktop app and the web app. vectorcraft-cli's main.rs is left without the attribute because an open PR (#13) edits that file. Co-Authored-By: Claude Opus 5.5 --- apps/vectorcraft-cli/src/perf.rs | 23 +++++++++++++++++++---- apps/vectorcraft-web/src/main.rs | 1 + apps/vectorcraft/src/main.rs | 1 + clippy.toml | 4 ++++ crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/menus.rs | 9 ++++++++- crates/ui-egui/src/panels/layers.rs | 2 +- crates/ui-egui/src/panels/swatches.rs | 4 +++- 9 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 clippy.toml diff --git a/apps/vectorcraft-cli/src/perf.rs b/apps/vectorcraft-cli/src/perf.rs index 3ab22a11..40621d55 100644 --- a/apps/vectorcraft-cli/src/perf.rs +++ b/apps/vectorcraft-cli/src/perf.rs @@ -43,7 +43,9 @@ pub fn synthetic(n: usize) -> Document { if i % 3 == 1 { node.opacity = 0.8; } - d.insert(Some(l), usize::MAX, node).expect("insert"); + if d.insert(Some(l), usize::MAX, node).is_err() { + break; + } } d } @@ -122,8 +124,17 @@ pub fn run(args: &[String]) -> Result<(), String> { rows.push(("hit test (per click)", hits, 2.0)); let mut bytes = vec![]; + let mut failed: Option = None; rows.push(("save .vectorcraft", median_ms(3, || bytes = vectorcraft_format::save_file(&doc)), 300.0)); - rows.push(("open .vectorcraft", median_ms(3, || drop(vectorcraft_format::load(&bytes).expect("load"))), 300.0)); + rows.push(( + "open .vectorcraft", + median_ms(3, || { + if let Err(e) = vectorcraft_format::load(&bytes) { + failed = Some(format!("open .vectorcraft: {e}")); + } + }), + 300.0, + )); let svg_opts = vectorcraft_svg::ExportOptions::default(); rows.push(("export SVG", median_ms(3, || drop(vectorcraft_svg::export(&doc, &svg_opts))), 500.0)); @@ -131,10 +142,14 @@ pub fn run(args: &[String]) -> Result<(), String> { let unite = median_ms(3, || { let mut s = vectorcraft_engine::Session::new(); s.add_document(synthetic(1000), None); - s.execute("select.all", &json!({})).expect("select"); - s.execute("object.pathfinder.unite", &json!({})).expect("unite"); + if let Err(e) = s.execute("select.all", &json!({})).and_then(|_| s.execute("object.pathfinder.unite", &json!({}))) { + failed = Some(format!("Pathfinder Unite: {e}")); + } }); rows.push(("Pathfinder Unite, 1,000 paths", unite, 150.0)); + if let Some(e) = failed { + return Err(e); + } let mut over = 0; println!(" {:<40} {:>10} {:>10}", "", "measured", "budget"); diff --git a/apps/vectorcraft-web/src/main.rs b/apps/vectorcraft-web/src/main.rs index 945f83d4..903d2be6 100644 --- a/apps/vectorcraft-web/src/main.rs +++ b/apps/vectorcraft-web/src/main.rs @@ -12,6 +12,7 @@ //! - dropped files are read asynchronously by `web::WebShell` and delivered through the inbox. //! //! URL query flag: `?webgl` forces the WebGL2 backend instead of WebGPU. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #[cfg(target_arch = "wasm32")] mod web; diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 8d35e65d..7f6eb44b 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -5,6 +5,7 @@ //! `--control ` (or `VECTORCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: //! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`. //! See `vectorcraft_ui_egui::control` for the methods. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod control_server; #[cfg(target_os = "macos")] diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 7bb4c3d2..a8ef7179 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -107,7 +107,7 @@ pub fn fit(app: &mut VectorcraftApp, how: &str) { _ => st.doc.artboards.first().map(|a| a.rect), }; let Some(target) = target else { return }; - let v = app.view_mut().unwrap(); + let Some(v) = app.view_mut() else { return }; v.center = target.center(); v.fitted = true; if how == "view.actualSize" { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 673c65bd..d9c9799c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -4,6 +4,7 @@ //! (view/window) here and everything else to the engine. The same entry point serves menus, //! shortcuts, the ⌘K palette and the control channel ([`control`]). #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod brand; pub mod canvas; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 4fd22900..5411ec2f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1574,7 +1574,14 @@ pub fn pretty_shortcut(s: &str) -> String { if cfg!(target_os = "macos") { let mut mods = String::new(); let parts: Vec<&str> = s.split('+').collect(); - let (key, ms) = if s.ends_with("++") { ("+", &parts[..parts.len() - 2]) } else { (*parts.last().unwrap(), &parts[..parts.len() - 1]) }; + let (key, ms) = if s.ends_with("++") { + ("+", parts.get(..parts.len().saturating_sub(2)).unwrap_or_default()) + } else { + match parts.split_last() { + Some((key, ms)) => (*key, ms), + None => (s, &[][..]), + } + }; for m in ["Ctrl", "Alt", "Shift", "Cmd"] { if ms.contains(&m) { mods.push_str(match m { diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 8988d335..699cbbbf 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -275,7 +275,7 @@ fn row( } if has_children && expanded.contains(&n.id.0) { - for (i, c) in n.children().unwrap().iter().enumerate().rev() { + for (i, c) in n.children().map_or(&[][..], Vec::as_slice).iter().enumerate().rev() { row(ui, doc, c, depth + 1, i == 0 && n.clips(), sel, current, expanded, actions, t); } } diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 12f6fb42..550bf757 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -676,7 +676,9 @@ pub(crate) fn tile_grid<'a, T>(ui: &mut Ui, items: &'a [T], view: View, is_folde if len == per_row || (len > 0 && is_folder(e)) { rows.push(vec![]); } - rows.last_mut().expect("a row").push(e); + if let Some(row) = rows.last_mut() { + row.push(e); + } } let mut out = Vec::with_capacity(items.len()); for row in rows {