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:
+35
-8
@@ -1,7 +1,10 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use std::path::PathBuf;
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
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 {
|
fn is_form_type(ty: &Type) -> bool {
|
||||||
match ty {
|
let Type::Path(path) = ty else {
|
||||||
Type::Path(path) => path.path.segments.last().is_some_and(|segment| segment.ident == "Form"),
|
return false;
|
||||||
_ => 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 {
|
fn has_non_unit_return(function: &ItemFn) -> bool {
|
||||||
@@ -332,11 +352,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_param_matches_form_type_not_name_suffix() {
|
fn form_param_matches_form_type_not_name_suffix() {
|
||||||
let real_form: Type = parse_quote!(slhx::Form<CreateTodo>);
|
// req: form/004 req: form/006
|
||||||
let impostor: Type = parse_quote!(CreateTodoForm);
|
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(&qualified_form));
|
||||||
assert!(!is_form_type(&impostor));
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -324,6 +324,60 @@ fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_handle_rejects_nongeneric_form_impostor() {
|
||||||
|
// req: form/004 req: form/006 req: test/003
|
||||||
|
let fixture = Fixture::new("slhx-derive-form-handler-nongeneric-impostor-fail");
|
||||||
|
fixture.write(
|
||||||
|
"Cargo.toml",
|
||||||
|
&format!(
|
||||||
|
r#"[package]
|
||||||
|
name = "slhx-derive-form-handler-nongeneric-impostor-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() {
|
||||||
|
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||||
|
std::fs::write(
|
||||||
|
out.join("slhx.syms"),
|
||||||
|
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"src/lib.rs",
|
||||||
|
r#"struct Form;
|
||||||
|
|
||||||
|
#[slhx::handler]
|
||||||
|
fn create(_form: Form) -> 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 `create` handles a generated form and must accept slhx::Form<_>"),
|
||||||
|
"missing nongeneric form diagnostic in stderr:\n{stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn form_handle_rejects_form_name_suffix_impostor() {
|
fn form_handle_rejects_form_name_suffix_impostor() {
|
||||||
// req: form/004 req: form/006 req: test/003
|
// req: form/004 req: form/006 req: test/003
|
||||||
|
|||||||
Reference in New Issue
Block a user