feat(derive): reject empty handlers

req: derive_handler/001
This commit is contained in:
slhx agent
2026-05-10 23:38:29 +02:00
parent 4e971d9597
commit 61da7a7b24
+58 -11
View File
@@ -1,7 +1,7 @@
use proc_macro::TokenStream;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, ItemFn};
use syn::{parse_macro_input, FnArg, ItemFn, ReturnType, Type};
#[proc_macro_attribute]
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
@@ -9,15 +9,27 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
let name = function.sig.ident.to_string();
if let Some(syms_path) = syms_path() {
if syms_path.exists() && !syms_contains_handle(&syms_path, &name) {
let message = format!(
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
);
return quote!(
#function
compile_error!(#message);
)
.into();
if syms_path.exists() {
if !syms_contains_handle(&syms_path, &name) {
let message = format!(
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
);
return quote!(
#function
compile_error!(#message);
)
.into();
}
if !has_form_param(&function) && !has_non_unit_return(&function) {
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();
}
}
}
@@ -72,6 +84,27 @@ fn generated_path(file: &str) -> Option<PathBuf> {
std::env::var_os("OUT_DIR").map(|out_dir| PathBuf::from(out_dir).join(file))
}
fn has_form_param(function: &ItemFn) -> bool {
function.sig.inputs.iter().any(|arg| match arg {
FnArg::Typed(arg) => type_ends_with(&arg.ty, "Form"),
FnArg::Receiver(_) => false,
})
}
fn has_non_unit_return(function: &ItemFn) -> bool {
match &function.sig.output {
ReturnType::Default => false,
ReturnType::Type(_, ty) => !matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty()),
}
}
fn type_ends_with(ty: &Type, ident: &str) -> bool {
match ty {
Type::Path(path) => path.path.segments.last().is_some_and(|segment| segment.ident == ident),
_ => false,
}
}
fn syms_contains_handle(path: &PathBuf, ident: &str) -> bool {
let Ok(syms) = std::fs::read_to_string(path) else {
return true;
@@ -91,7 +124,8 @@ fn compile_error(message: &str) -> TokenStream {
#[cfg(test)]
mod tests {
use super::syms_contains_handle;
use super::{has_form_param, has_non_unit_return, syms_contains_handle};
use syn::parse_quote;
#[test]
fn syms_lookup_matches_handle_ident() {
@@ -107,4 +141,17 @@ mod tests {
let _ = std::fs::remove_file(path);
}
#[test]
fn handler_shape_accepts_form_or_effect_return() {
// req: derive_handler/001
let with_form = parse_quote!(fn save(form: slhx::Form<String>) {});
let with_return = parse_quote!(fn ping() -> impl slhx::IntoEffect { slhx::EffectBatch::default() });
let empty = parse_quote!(fn noop() {});
assert!(has_form_param(&with_form));
assert!(has_non_unit_return(&with_return));
assert!(!has_form_param(&empty));
assert!(!has_non_unit_return(&empty));
}
}