From 2974ab28cbf79dd445c4b78d81d358be7682abae Mon Sep 17 00:00:00 2001 From: Kaan Barmore-Genc Date: Mon, 29 Jun 2026 01:46:09 -0500 Subject: [PATCH 1/3] Convert macro panics to compile errors The RustEmbed derive macro previously called panic!() and .unwrap() on misconfiguration (non-unit struct, missing/duplicate folder, bad path interpolation, missing CARGO_MANIFEST_DIR, multiple prefixes). These produced opaque macro-expansion panics. Thread syn::Result through impl_rust_embed_for_web and emit syn::Error::new_spanned(...).to_compile_error() so users get proper compile-time diagnostics pointing at the derive instead of a panic. Ports upstream rust-embed commit 7a14d89. Claude-Session: https://claude.ai/code/session_01MixCkrYqoVEZZbPRRNtL3M --- impl/src/lib.rs | 62 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 8a0d064..8a5119c 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -41,29 +41,57 @@ fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec .collect() } -fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> TokenStream2 { +fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result { match ast.data { Data::Struct(ref data) => match data.fields { Fields::Unit => {} - _ => panic!("RustEmbed can only be derived for unit structs"), + _ => { + return Err(syn::Error::new_spanned( + ast, + "RustEmbed can only be derived for unit structs", + )) + } }, - _ => panic!("RustEmbed can only be derived for unit structs"), + _ => { + return Err(syn::Error::new_spanned( + ast, + "RustEmbed can only be derived for unit structs", + )) + } }; let mut folder_paths = find_attribute_values(ast, "folder"); if folder_paths.len() != 1 { - panic!("#[derive(RustEmbed)] must contain one and only one folder attribute"); + return Err(syn::Error::new_spanned( + ast, + "#[derive(RustEmbed)] must contain one and only one folder attribute", + )); } let folder_path = folder_paths.remove(0); #[cfg(feature = "interpolate-folder-path")] - let folder_path = shellexpand::full(&folder_path).unwrap().to_string(); + let folder_path = shellexpand::full(&folder_path) + .map_err(|e| { + syn::Error::new_spanned(ast, format!("Could not interpolate folder path: {e}")) + })? + .to_string(); // Base relative paths on the Cargo.toml location let folder_path = if Path::new(&folder_path).is_relative() { - Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) + let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|e| { + syn::Error::new_spanned( + ast, + format!("Could not read the CARGO_MANIFEST_DIR environment variable: {e}"), + ) + })?; + Path::new(&manifest_dir) .join(folder_path) .to_str() - .unwrap() + .ok_or_else(|| { + syn::Error::new_spanned( + ast, + "The folder path does not have a valid string representation", + ) + })? .to_owned() } else { folder_path @@ -88,13 +116,16 @@ fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> TokenStream2 { } else if prefixes.len() == 1 { prefixes[0].clone() } else { - panic!("#[derive(RustEmbed)] must have at most one prefix, you supplied several"); + return Err(syn::Error::new_spanned( + ast, + "#[derive(RustEmbed)] must have at most one prefix, you supplied several", + )); }; if cfg!(debug_assertions) && !cfg!(feature = "always-embed") { - generate_dynamic_impl(&ast.ident, &config, &folder_path, &prefix) + Ok(generate_dynamic_impl(&ast.ident, &config, &folder_path, &prefix)) } else { - generate_embed_impl(&ast.ident, &config, &folder_path, &prefix) + Ok(generate_embed_impl(&ast.ident, &config, &folder_path, &prefix)) } } @@ -118,7 +149,12 @@ fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> TokenStream2 { /// /// Please check the package readme for more details. pub fn derive_input_object(input: TokenStream) -> TokenStream { - let ast: DeriveInput = syn::parse(input).unwrap(); - let gen = impl_rust_embed_for_web(&ast); - gen.into() + let ast: DeriveInput = match syn::parse(input) { + Ok(ast) => ast, + Err(e) => return e.to_compile_error().into(), + }; + match impl_rust_embed_for_web(&ast) { + Ok(gen) => gen.into(), + Err(e) => e.to_compile_error().into(), + } } From f3b8a75e1eb9fa5d8b01ddc0e843d7b63ab80a50 Mon Sep 17 00:00:00 2001 From: Kaan Barmore-Genc Date: Mon, 29 Jun 2026 20:09:50 -0500 Subject: [PATCH 2/3] Format derive impl and cover macro error paths with tests Apply rustfmt to the multi-argument generate_*_impl calls so the format check passes, and add unit tests that exercise impl_rust_embed_for_web directly: enum target, struct with fields, missing folder attribute, duplicate folder attributes, duplicate prefix attributes, and a valid unit struct. These cover the new syn::Error compile-error paths that the patch added. Claude-Session: https://claude.ai/code/session_01MixCkrYqoVEZZbPRRNtL3M --- impl/src/lib.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 8a5119c..4b07bb2 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -123,9 +123,19 @@ fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result }; if cfg!(debug_assertions) && !cfg!(feature = "always-embed") { - Ok(generate_dynamic_impl(&ast.ident, &config, &folder_path, &prefix)) + Ok(generate_dynamic_impl( + &ast.ident, + &config, + &folder_path, + &prefix, + )) } else { - Ok(generate_embed_impl(&ast.ident, &config, &folder_path, &prefix)) + Ok(generate_embed_impl( + &ast.ident, + &config, + &folder_path, + &prefix, + )) } } @@ -158,3 +168,56 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { Err(e) => e.to_compile_error().into(), } } + +#[cfg(test)] +mod tests { + use super::impl_rust_embed_for_web; + + fn err_message(input: &str) -> String { + let ast: syn::DeriveInput = syn::parse_str(input).unwrap(); + impl_rust_embed_for_web(&ast) + .expect_err("expected the derive input to fail") + .to_string() + } + + #[test] + fn rejects_enums() { + assert!(err_message("#[folder = \"src\"] enum Bad { A }") + .contains("can only be derived for unit structs")); + } + + #[test] + fn rejects_structs_with_fields() { + assert!(err_message("#[folder = \"src\"] struct Bad { field: u32 }") + .contains("can only be derived for unit structs")); + } + + #[test] + fn requires_a_folder_attribute() { + assert!(err_message("struct Bad;").contains("one and only one folder attribute")); + } + + #[test] + fn rejects_multiple_folder_attributes() { + assert!( + err_message("#[folder = \"src\"] #[folder = \"src\"] struct Bad;") + .contains("one and only one folder attribute") + ); + } + + #[test] + fn rejects_multiple_prefix_attributes() { + assert!(err_message( + "#[folder = \"src\"] #[prefix = \"a/\"] #[prefix = \"b/\"] struct Bad;" + ) + .contains("at most one prefix")); + } + + #[test] + fn accepts_a_valid_unit_struct() { + // `src` exists relative to this crate's Cargo.toml, so the derive + // resolves the folder and emits an implementation. + let ast: syn::DeriveInput = syn::parse_str("#[folder = \"src\"] struct Good;").unwrap(); + assert!(impl_rust_embed_for_web(&ast).is_ok()); + } +} From ad6dc85a7357de6ed416ab72eb9f2e0ea7d7c936 Mon Sep 17 00:00:00 2001 From: Kaan Barmore-Genc Date: Mon, 29 Jun 2026 20:13:47 -0500 Subject: [PATCH 3/3] Convert the missing-folder panic to a compile error The allow_missing change on master still panicked when the folder is missing and the opt-in is not set. Return a syn::Error like the other misconfiguration paths so it surfaces as a normal compile error. Add tests for both the missing-folder error and the allow_missing opt-in success path. Claude-Session: https://claude.ai/code/session_01MixCkrYqoVEZZbPRRNtL3M --- impl/src/lib.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 4b07bb2..fc30538 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -102,12 +102,14 @@ fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> syn::Result // If the folder does not exist, either fail the build or, when the // `allow_missing` attribute is set, generate an empty asset set. if !Path::new(&folder_path).exists() && !config.allow_missing() { - panic!( - "#[derive(RustEmbed)] folder '{}' does not exist. \ - Set `#[allow_missing = true]` to allow a missing folder and \ - generate an empty asset set instead.", - folder_path - ); + return Err(syn::Error::new_spanned( + ast, + format!( + "#[derive(RustEmbed)] folder '{folder_path}' does not exist. \ + Set `#[allow_missing = true]` to allow a missing folder and \ + generate an empty asset set instead." + ), + )); } let prefixes = find_attribute_values(ast, "prefix"); @@ -213,6 +215,21 @@ mod tests { .contains("at most one prefix")); } + #[test] + fn rejects_a_missing_folder() { + assert!( + err_message("#[folder = \"does-not-exist\"] struct Bad;").contains("does not exist") + ); + } + + #[test] + fn allows_a_missing_folder_when_opted_in() { + let ast: syn::DeriveInput = + syn::parse_str("#[folder = \"does-not-exist\"] #[allow_missing = true] struct Good;") + .unwrap(); + assert!(impl_rust_embed_for_web(&ast).is_ok()); + } + #[test] fn accepts_a_valid_unit_struct() { // `src` exists relative to this crate's Cargo.toml, so the derive