fix(derive): reject form impostor parameters

Tighten generated-form handler checking so a parameter must look like slhx::Form<T> or imported Form<T>, not a nongeneric local Form or unrelated path with a Form suffix.

req: form/004

req: form/006
This commit is contained in:
slhx agent
2026-05-25 23:31:48 +02:00
parent d02c15c59b
commit 34d6757ccd
2 changed files with 89 additions and 8 deletions
+35 -8
View File
@@ -1,7 +1,10 @@
use proc_macro::TokenStream;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, FnArg, Item, ItemFn, ItemMod, Pat, ReturnType, Type};
use syn::{
parse_macro_input, FnArg, GenericArgument, Item, ItemFn, ItemMod, Pat, PathArguments,
ReturnType, Type,
};
#[proc_macro_attribute]
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
@@ -165,10 +168,27 @@ fn has_form_param(function: &ItemFn) -> bool {
}
fn is_form_type(ty: &Type) -> bool {
match ty {
Type::Path(path) => path.path.segments.last().is_some_and(|segment| segment.ident == "Form"),
_ => false,
let Type::Path(path) = ty else {
return false;
};
let mut segments = path.path.segments.iter();
let Some(first) = segments.next() else {
return false;
};
let last = path.path.segments.last().expect("path has at least one segment");
let path_is_form = if path.path.segments.len() == 1 {
first.ident == "Form"
} else {
first.ident == "slhx" && last.ident == "Form"
};
if !path_is_form {
return false;
}
matches!(
&last.arguments,
PathArguments::AngleBracketed(args)
if args.args.iter().any(|arg| matches!(arg, GenericArgument::Type(_)))
)
}
fn has_non_unit_return(function: &ItemFn) -> bool {
@@ -332,11 +352,18 @@ mod tests {
#[test]
fn form_param_matches_form_type_not_name_suffix() {
let real_form: Type = parse_quote!(slhx::Form<CreateTodo>);
let impostor: Type = parse_quote!(CreateTodoForm);
// req: form/004 req: form/006
let qualified_form: Type = parse_quote!(slhx::Form<CreateTodo>);
let imported_form: Type = parse_quote!(Form<CreateTodo>);
let name_suffix_impostor: Type = parse_quote!(CreateTodoForm);
let nongeneric_impostor: Type = parse_quote!(Form);
let foreign_form: Type = parse_quote!(other::Form<CreateTodo>);
assert!(is_form_type(&real_form));
assert!(!is_form_type(&impostor));
assert!(is_form_type(&qualified_form));
assert!(is_form_type(&imported_form));
assert!(!is_form_type(&name_suffix_impostor));
assert!(!is_form_type(&nongeneric_impostor));
assert!(!is_form_type(&foreign_form));
}
#[test]