fix(derive): reject form type impostors
Tighten generated form-handler validation so only actual Form paths satisfy form handles; names like CreateForm no longer bypass the slhx::Form<_> requirement. Cover with unit and compile-fail tests. req: form/004 req: form/006 req: test/003
This commit is contained in:
+19
-10
@@ -120,11 +120,18 @@ fn generated_path(file: &str) -> Option<PathBuf> {
|
||||
|
||||
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::Typed(arg) => is_form_type(&arg.ty),
|
||||
FnArg::Receiver(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_form_type(ty: &Type) -> bool {
|
||||
match ty {
|
||||
Type::Path(path) => path.path.segments.last().is_some_and(|segment| segment.ident == "Form"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_non_unit_return(function: &ItemFn) -> bool {
|
||||
match &function.sig.output {
|
||||
ReturnType::Default => false,
|
||||
@@ -132,13 +139,6 @@ fn has_non_unit_return(function: &ItemFn) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -169,8 +169,8 @@ fn compile_error(message: &str) -> TokenStream {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{handle_requires_form, has_form_param, has_non_unit_return, syms_contains_handle};
|
||||
use syn::parse_quote;
|
||||
use super::{handle_requires_form, has_form_param, has_non_unit_return, is_form_type, syms_contains_handle};
|
||||
use syn::{parse_quote, Type};
|
||||
|
||||
#[test]
|
||||
fn syms_lookup_matches_handle_ident() {
|
||||
@@ -201,4 +201,13 @@ mod tests {
|
||||
assert!(!has_form_param(&empty));
|
||||
assert!(!has_non_unit_return(&empty));
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
assert!(is_form_type(&real_form));
|
||||
assert!(!is_form_type(&impostor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,60 @@ fn create() -> impl slhx::IntoEffect {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_rejects_form_name_suffix_impostor() {
|
||||
// req: form/004 req: form/006 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-form-impostor-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-form-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 CreateForm;
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: CreateForm) -> 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 form-impostor diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_macro_reports_missing_generated_include() {
|
||||
// req: build/004 req: test/003
|
||||
|
||||
Reference in New Issue
Block a user