fix(derive): require generated handler symbols

Make #[slhx::handler] fail when slhx.syms is missing instead of silently skipping generated handle checks. Add compile-fail coverage with an actionable build.rs hint.

req: build/003

req: test/003
This commit is contained in:
slhx agent
2026-05-25 21:26:48 +02:00
parent dca26511cc
commit 87a19fd923
2 changed files with 99 additions and 33 deletions
+48 -33
View File
@@ -8,39 +8,54 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn); let function = parse_macro_input!(item as ItemFn);
let name = function.sig.ident.to_string(); let name = function.sig.ident.to_string();
if let Some(syms_path) = syms_path() { let Some(syms_path) = syms_path() else {
if syms_path.exists() { let message = "#[slhx::handler] requires OUT_DIR; run inside a Cargo crate with slhx_build::app() in build.rs";
if !syms_contains_handle(&syms_path, &name) { return quote!(
let message = format!( #function
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler" compile_error!(#message);
); )
return quote!( .into();
#function };
compile_error!(#message); if !syms_path.exists() {
) let message = format!(
.into(); "#[slhx::handler] could not find {}; add slhx_build::app().run()? to build.rs or check template generation",
} syms_path.display()
if !has_form_param(&function) && !has_non_unit_return(&function) { );
let message = format!( return quote!(
"slhx handler `{name}` must accept a form/context parameter or return a value implementing IntoEffect" #function
); compile_error!(#message);
return quote!( )
#function .into();
compile_error!(#message); }
) if !syms_contains_handle(&syms_path, &name) {
.into(); let message = format!(
} "unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
if handle_requires_form(&syms_path, &name) && !has_form_param(&function) { );
let message = format!( return quote!(
"slhx handler `{name}` handles a generated form and must accept slhx::Form<_>" #function
); compile_error!(#message);
return quote!( )
#function .into();
compile_error!(#message); }
) if !has_form_param(&function) && !has_non_unit_return(&function) {
.into(); let message = format!(
} "slhx handler `{name}` must accept a form/context parameter or return a value implementing IntoEffect"
} );
return quote!(
#function
compile_error!(#message);
)
.into();
}
if handle_requires_form(&syms_path, &name) && !has_form_param(&function) {
let message = format!(
"slhx handler `{name}` handles a generated form and must accept slhx::Form<_>"
);
return quote!(
#function
compile_error!(#message);
)
.into();
} }
quote!(#function).into() quote!(#function).into()
+51
View File
@@ -60,6 +60,57 @@ fn known() {}
); );
} }
#[test]
fn handler_macro_reports_missing_syms() {
// req: build/003 req: test/003
let fixture = Fixture::new("slhx-derive-handler-syms-fail");
fixture.write(
"Cargo.toml",
&format!(
r#"[package]
name = "slhx-derive-handler-syms-fail"
version = "0.0.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[dependencies]
slhx = {{ path = {:?} }}
"#,
repo_path("slhx")
),
);
fixture.write(
"build.rs",
r#"fn main() {
println!("cargo:rerun-if-changed=build.rs");
}
"#,
);
fixture.write(
"src/lib.rs",
r#"#[slhx::handler]
fn create() -> impl slhx::IntoEffect {
slhx::EffectBatch::default()
}
"#,
);
let output = check_fixture(&fixture);
assert!(!output.status.success(), "fixture unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("#[slhx::handler] could not find"),
"missing handler syms diagnostic in stderr:\n{stderr}"
);
assert!(
stderr.contains("add slhx_build::app().run()? to build.rs"),
"missing build.rs hint in stderr:\n{stderr}"
);
}
#[test] #[test]
fn form_handle_requires_form_parameter() { fn form_handle_requires_form_parameter() {
// req: form/004 req: form/006 req: test/003 // req: form/004 req: form/006 req: test/003