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 3ab22a119..40621d557 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 945f83d49..903d2be60 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 8d35e65d2..7f6eb44bc 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 000000000..4b6c79f55 --- /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 7bb4c3d27..a8ef71795 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 673c65bde..d9c9799c6 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 4fd22900b..5411ec2f9 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 8988d335b..699cbbbfe 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 12f6fb42d..550bf7572 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 {