From 71abe2567d2894db54c203d4a99f70845d813b9a Mon Sep 17 00:00:00 2001 From: Wietse Date: Wed, 13 May 2026 19:31:06 +0200 Subject: [PATCH 1/3] xee-xpath-macros: replace panic sites with spanned errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the panic / unwrap sites in xee-xpath-macros to bail_spanned! errors so that bad #[xpath_fn] signatures produce compile errors pointing at the user's code, instead of bare panics at macro expansion time. Sites converted: - parse.rs:49 — `expect("Signature not found")` when #[xpath_fn] is invoked with keyword args only (no signature string). - convert.rs — six panic sites for unsupported signature shapes: NonEmpty occurrence (`+`), named element tests (`element(foo)`), non-`element` / non-`item` kind tests, typed array tests, typed map tests, and atomic types without a Rust wrapper. The convert.rs sites threaded `fn_arg: &syn::FnArg` through the internal helpers so each error's span points at the offending Rust argument. The public surface (`convert_sequence_type`) is unchanged; only internal helpers gained a parameter. Adds insta snapshot tests for each converted site, pinning the error message text and confirming the error path is reached rather than the panic. No semantic change to the macro's happy path. Conformance suite unchanged: Passed 20221, Failed 0, Error 0. --- xee-xpath-macros/src/convert.rs | 111 +++++++++++++++--- xee-xpath-macros/src/parse.rs | 14 ++- ...s__convert_attribute_kind_test_errors.snap | 5 + ...ts__convert_named_element_test_errors.snap | 5 + ...s__convert_nonempty_occurrence_errors.snap | 5 + ..._tests__convert_text_kind_test_errors.snap | 5 + ...rt__tests__convert_typed_array_errors.snap | 5 + ...vert__tests__convert_typed_map_errors.snap | 5 + ...rse__tests__parse_keyword_only_errors.snap | 9 ++ 9 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_attribute_kind_test_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_nonempty_occurrence_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_text_kind_test_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_array_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_map_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__parse__tests__parse_keyword_only_errors.snap diff --git a/xee-xpath-macros/src/convert.rs b/xee-xpath-macros/src/convert.rs index 69e221519..cdc6f71e3 100644 --- a/xee-xpath-macros/src/convert.rs +++ b/xee-xpath-macros/src/convert.rs @@ -3,6 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; +use syn::spanned::Spanned; use xee_schema_type::Xs; use xee_xpath_ast::ast; @@ -28,7 +29,7 @@ fn convert_item( name: TokenStream, arg: TokenStream, ) -> syn::Result { - let (iterator, borrow) = convert_item_type(&item.item_type, arg.clone())?; + let (iterator, borrow) = convert_item_type(&item.item_type, fn_arg, arg.clone())?; Ok(match &item.occurrence { ast::Occurrence::One => { @@ -93,42 +94,65 @@ fn convert_item( // let #name = #name_temp.as_slice(); // ) } - ast::Occurrence::NonEmpty => todo!("NonEmpty not yet supported"), + ast::Occurrence::NonEmpty => bail_spanned!( + fn_arg.span() => + "one-or-more occurrence (`+`) is not yet supported in #[xpath_fn] signatures" + ), }) } -fn convert_item_type(item: &ast::ItemType, arg: TokenStream) -> syn::Result<(TokenStream, bool)> { +fn convert_item_type( + item: &ast::ItemType, + fn_arg: &syn::FnArg, + arg: TokenStream, +) -> syn::Result<(TokenStream, bool)> { match item { ast::ItemType::Item => Ok((quote!(#arg.iter().map(Ok)), false)), ast::ItemType::AtomicOrUnionType(xs) => { - let (token_stream, borrow) = convert_atomic_or_union_type(*xs, arg)?; + let (token_stream, borrow) = convert_atomic_or_union_type(*xs, fn_arg, arg)?; Ok((token_stream, borrow)) } - ast::ItemType::KindTest(kind_test) => Ok((convert_kind_test(kind_test, arg)?, false)), + ast::ItemType::KindTest(kind_test) => { + Ok((convert_kind_test(kind_test, fn_arg, arg)?, false)) + } // we don't do anything special for higher order functions at this point; // the implementation is supposed to manually unpack the items ast::ItemType::FunctionTest(_) => Ok((quote!(#arg.iter().map(Ok)), false)), ast::ItemType::ArrayTest(array_test) => match array_test { ast::ArrayTest::AnyArrayTest => Ok((quote!(#arg.array_iter()), false)), - _ => todo!("Unsupported item type: typed array test"), + _ => bail_spanned!( + fn_arg.span() => + "typed array tests (e.g. `array(xs:integer)`) are not yet supported in #[xpath_fn] signatures — use `array(*)`" + ), }, ast::ItemType::MapTest(map_test) => match map_test { ast::MapTest::AnyMapTest => Ok((quote!(#arg.map_iter()), false)), - _ => todo!("Unsupported item type: typed map test"), + _ => bail_spanned!( + fn_arg.span() => + "typed map tests (e.g. `map(xs:string, xs:integer)`) are not yet supported in #[xpath_fn] signatures — use `map(*)`" + ), }, } } -fn convert_atomic_or_union_type(xs: Xs, arg: TokenStream) -> syn::Result<(TokenStream, bool)> { +fn convert_atomic_or_union_type( + xs: Xs, + fn_arg: &syn::FnArg, + arg: TokenStream, +) -> syn::Result<(TokenStream, bool)> { if xs == Xs::AnyAtomicType || xs == Xs::Numeric { return Ok((quote!(#arg.atomized(interpreter.xot())), false)); } - // TODO: another unwrap that should really be "we cannot create a rust wrapper - // for this type" error - let rust_info = xs - .rust_info() - .expect("Rust wrapper for this type not found"); + let Some(rust_info) = xs.rust_info() else { + bail_spanned!( + fn_arg.span() => + format!( + "XPath atomic type `{xs:?}` has no Rust wrapper — cannot bridge it through #[xpath_fn]. \ + If this type should be supported, register it in xee_schema_type::Xs::rust_info" + ) + ); + }; let type_name = rust_info.rust_name(); let type_name = syn::parse_str::(type_name)?; let convert = quote!(std::convert::TryInto::<#type_name>::try_into(atomic)); @@ -140,18 +164,26 @@ fn convert_atomic_or_union_type(xs: Xs, arg: TokenStream) -> syn::Result<(TokenS )) } -fn convert_kind_test(kind_test: &ast::KindTest, arg: TokenStream) -> syn::Result { +fn convert_kind_test( + kind_test: &ast::KindTest, + fn_arg: &syn::FnArg, + arg: TokenStream, +) -> syn::Result { match kind_test { ast::KindTest::Any => Ok(quote!(#arg.nodes())), ast::KindTest::Element(element_test) => { if element_test.is_some() { - unreachable!("Unsupported element test") + bail_spanned!( + fn_arg.span() => + "named element tests (e.g. `element(foo)`) are not yet supported in #[xpath_fn] signatures — use `element()`" + ); } Ok(quote!(#arg.elements(interpreter.xot())?)) } - _ => { - todo!("Unsupported kind test") - } + _ => bail_spanned!( + fn_arg.span() => + "this kind test is not yet supported in #[xpath_fn] signatures — only `node()`, `element()`, and `item()` work today" + ), } } @@ -262,4 +294,47 @@ mod tests { fn test_convert_array() { assert_debug_snapshot!(convert("array(*)")); } + + fn convert_err(s: &str) -> String { + let fn_arg: syn::FnArg = syn::parse_str("a: &str").unwrap(); + let namespaces = Namespaces::default(); + let sequence_type = parse_sequence_type(s, &namespaces).unwrap(); + let name = quote!(a); + let arg = quote!(arguments[0]); + + convert_sequence_type(&sequence_type, &fn_arg, name, arg) + .expect_err("expected a syn::Error from convert_sequence_type") + .to_string() + } + + #[test] + fn test_convert_nonempty_occurrence_errors() { + assert_debug_snapshot!(convert_err("xs:integer+")); + } + + #[test] + fn test_convert_named_element_test_errors() { + // element(foo) — named element tests aren't supported yet + assert_debug_snapshot!(convert_err("element(foo)")); + } + + #[test] + fn test_convert_attribute_kind_test_errors() { + assert_debug_snapshot!(convert_err("attribute()")); + } + + #[test] + fn test_convert_text_kind_test_errors() { + assert_debug_snapshot!(convert_err("text()")); + } + + #[test] + fn test_convert_typed_array_errors() { + assert_debug_snapshot!(convert_err("array(xs:integer)")); + } + + #[test] + fn test_convert_typed_map_errors() { + assert_debug_snapshot!(convert_err("map(xs:string, xs:integer)")); + } } diff --git a/xee-xpath-macros/src/parse.rs b/xee-xpath-macros/src/parse.rs index 294386d58..f163c3b2a 100644 --- a/xee-xpath-macros/src/parse.rs +++ b/xee-xpath-macros/src/parse.rs @@ -46,7 +46,12 @@ impl Parse for XPathFnOptions { } } } - let signature_string = signature.expect("Signature not found"); + let Some(signature_string) = signature else { + return Err(input.error( + "missing XPath signature string — #[xpath_fn(...)] requires a signature literal \ + like \"fn:foo($x as xs:integer) as xs:integer\" as one of its arguments", + )); + }; let namespaces = Namespaces::default(); let signature = Signature::parse(&signature_string, &namespaces) .map_err(|e| input.error(format!("{:?}", e)))?; @@ -134,4 +139,11 @@ mod tests { r#""fn:foo() as xs:string",blah"# )); } + + #[test] + fn test_parse_keyword_only_errors() { + // #[xpath_fn(context_first)] with no signature string should + // surface a clean error, not a panic. + assert_debug_snapshot!(syn::parse_str::(r#"context_first"#)); + } } diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_attribute_kind_test_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_attribute_kind_test_errors.snap new file mode 100644 index 000000000..b646a2911 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_attribute_kind_test_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"attribute()\")" +--- +"this kind test is not yet supported in #[xpath_fn] signatures — only `node()`, `element()`, and `item()` work today" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap new file mode 100644 index 000000000..5089d3f01 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"element(foo)\")" +--- +"named element tests (e.g. `element(foo)`) are not yet supported in #[xpath_fn] signatures — use `element()`" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_nonempty_occurrence_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_nonempty_occurrence_errors.snap new file mode 100644 index 000000000..9ec06db9d --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_nonempty_occurrence_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"xs:integer+\")" +--- +"one-or-more occurrence (`+`) is not yet supported in #[xpath_fn] signatures" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_text_kind_test_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_text_kind_test_errors.snap new file mode 100644 index 000000000..56631bd66 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_text_kind_test_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"text()\")" +--- +"this kind test is not yet supported in #[xpath_fn] signatures — only `node()`, `element()`, and `item()` work today" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_array_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_array_errors.snap new file mode 100644 index 000000000..27db6560d --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_array_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"array(xs:integer)\")" +--- +"typed array tests (e.g. `array(xs:integer)`) are not yet supported in #[xpath_fn] signatures — use `array(*)`" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_map_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_map_errors.snap new file mode 100644 index 000000000..42da1b35f --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_map_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"map(xs:string, xs:integer)\")" +--- +"typed map tests (e.g. `map(xs:string, xs:integer)`) are not yet supported in #[xpath_fn] signatures — use `map(*)`" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__parse__tests__parse_keyword_only_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__parse__tests__parse_keyword_only_errors.snap new file mode 100644 index 000000000..9640523d2 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__parse__tests__parse_keyword_only_errors.snap @@ -0,0 +1,9 @@ +--- +source: xee-xpath-macros/src/parse.rs +expression: "syn::parse_str::(r#\"context_first\"#)" +--- +Err( + Error( + "unexpected end of input, missing XPath signature string — #[xpath_fn(...)] requires a signature literal like \"fn:foo($x as xs:integer) as xs:integer\" as one of its arguments", + ), +) From 05ebb100a158aa71c14441c4c254dd81ba91186a Mon Sep 17 00:00:00 2001 From: Wietse Date: Wed, 13 May 2026 20:18:32 +0200 Subject: [PATCH 2/3] Fix arity-mismatch panic and over-narrow element-test message Two review findings on the first hardening pass: 1. (High) Arity mismatch between the XPath signature and the Rust function still panicked with a bare `index out of bounds` from `ast.sig.inputs[i + adjust]` in `make_wrapper`. The new `bail_spanned!` paths in convert.rs never got a chance to run for this case. Add an explicit bounds check after `adjust` is determined, surfacing a `syn::Error` pointing at the function name with both counts and (when relevant) a note about how many arguments were injected. Adds three regression tests: too-few Rust args, too-many Rust args, and a mismatch under `context` injection. 2. (Low) `convert_kind_test`'s diagnostic for `Element(Some(_))` reported "named element tests" but the AST shape (ElementOrAttributeTest with name_or_wildcard + optional type_name) also covers `element(*, xs:string)` and other typed/wildcard forms. Generalise the wording to "constrained element tests" with examples, and add a snapshot for `element(*, xs:string)` to lock in coverage. --- xee-xpath-macros/src/convert.rs | 14 ++++- ...ts__convert_named_element_test_errors.snap | 2 +- ...rt_typed_wildcard_element_test_errors.snap | 5 ++ ..._arity_mismatch_with_injected_context.snap | 5 ++ ...sts__wrapper_too_few_rust_args_errors.snap | 5 ++ ...ts__wrapper_too_many_rust_args_errors.snap | 5 ++ xee-xpath-macros/src/wrapper.rs | 63 +++++++++++++++++++ 7 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_wildcard_element_test_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_arity_mismatch_with_injected_context.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_few_rust_args_errors.snap create mode 100644 xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_many_rust_args_errors.snap diff --git a/xee-xpath-macros/src/convert.rs b/xee-xpath-macros/src/convert.rs index cdc6f71e3..509bf5809 100644 --- a/xee-xpath-macros/src/convert.rs +++ b/xee-xpath-macros/src/convert.rs @@ -175,7 +175,8 @@ fn convert_kind_test( if element_test.is_some() { bail_spanned!( fn_arg.span() => - "named element tests (e.g. `element(foo)`) are not yet supported in #[xpath_fn] signatures — use `element()`" + "constrained element tests (e.g. `element(foo)`, `element(*, xs:string)`) \ + are not yet supported in #[xpath_fn] signatures — use `element()`" ); } Ok(quote!(#arg.elements(interpreter.xot())?)) @@ -314,10 +315,19 @@ mod tests { #[test] fn test_convert_named_element_test_errors() { - // element(foo) — named element tests aren't supported yet + // element(foo) — name-constrained element tests aren't supported yet assert_debug_snapshot!(convert_err("element(foo)")); } + #[test] + fn test_convert_typed_wildcard_element_test_errors() { + // element(*, xs:string) — wildcard name + type constraint, + // which is still Element(Some(_)) in the AST. The error + // message must not mislead the user into thinking only + // *named* element tests are rejected. + assert_debug_snapshot!(convert_err("element(*, xs:string)")); + } + #[test] fn test_convert_attribute_kind_test_errors() { assert_debug_snapshot!(convert_err("attribute()")); diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap index 5089d3f01..32968c46a 100644 --- a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_named_element_test_errors.snap @@ -2,4 +2,4 @@ source: xee-xpath-macros/src/convert.rs expression: "convert_err(\"element(foo)\")" --- -"named element tests (e.g. `element(foo)`) are not yet supported in #[xpath_fn] signatures — use `element()`" +"constrained element tests (e.g. `element(foo)`, `element(*, xs:string)`) are not yet supported in #[xpath_fn] signatures — use `element()`" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_wildcard_element_test_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_wildcard_element_test_errors.snap new file mode 100644 index 000000000..eb90a503b --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__convert__tests__convert_typed_wildcard_element_test_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/convert.rs +expression: "convert_err(\"element(*, xs:string)\")" +--- +"constrained element tests (e.g. `element(foo)`, `element(*, xs:string)`) are not yet supported in #[xpath_fn] signatures — use `element()`" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_arity_mismatch_with_injected_context.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_arity_mismatch_with_injected_context.snap new file mode 100644 index 000000000..687834bd7 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_arity_mismatch_with_injected_context.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/wrapper.rs +expression: "xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()" +--- +"#[xpath_fn] arity mismatch: signature declares 0 XPath parameter(s), but the Rust function takes 1 (after subtracting 1 injected `context`/`interpreter` argument(s))" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_few_rust_args_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_few_rust_args_errors.snap new file mode 100644 index 000000000..4937ff663 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_few_rust_args_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/wrapper.rs +expression: "xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()" +--- +"#[xpath_fn] arity mismatch: signature declares 2 XPath parameter(s), but the Rust function takes 1" diff --git a/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_many_rust_args_errors.snap b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_many_rust_args_errors.snap new file mode 100644 index 000000000..2136b3e62 --- /dev/null +++ b/xee-xpath-macros/src/snapshots/xee_xpath_macros__wrapper__tests__wrapper_too_many_rust_args_errors.snap @@ -0,0 +1,5 @@ +--- +source: xee-xpath-macros/src/wrapper.rs +expression: "xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()" +--- +"#[xpath_fn] arity mismatch: signature declares 1 XPath parameter(s), but the Rust function takes 2" diff --git a/xee-xpath-macros/src/wrapper.rs b/xee-xpath-macros/src/wrapper.rs index f8a9eee08..499e501cf 100644 --- a/xee-xpath-macros/src/wrapper.rs +++ b/xee-xpath-macros/src/wrapper.rs @@ -75,6 +75,26 @@ fn make_wrapper( conversion_names.push(interpreter_ident); adjust += 1; } + + let expected_inputs = signature.params.len() + adjust; + if ast.sig.inputs.len() != expected_inputs { + let sig_count = signature.params.len(); + let rust_user_count = ast.sig.inputs.len().saturating_sub(adjust); + let injected_note = match adjust { + 0 => String::new(), + n => format!( + " (after subtracting {n} injected `context`/`interpreter` argument(s))" + ), + }; + bail_spanned!( + ast.sig.ident.span() => + format!( + "#[xpath_fn] arity mismatch: signature declares {sig_count} XPath parameter(s), \ + but the Rust function takes {rust_user_count}{injected_note}" + ) + ); + } + for (i, param) in signature.params.iter().enumerate() { let name = Ident::new(param.name.local_name(), Span::call_site()); conversion_names.push(name.clone()); @@ -187,4 +207,47 @@ mod tests { .unwrap(); assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap().to_string()); } + + #[test] + fn test_wrapper_too_few_rust_args_errors() { + // signature declares two XPath parameters but the Rust fn only + // takes one — used to panic with index-out-of-bounds; now should + // surface as a clean syn::Error pointing at the fn name. + let options = parse_str::( + r#""fn:foo($x as xs:int, $y as xs:int) as xs:string""#, + ) + .unwrap(); + let ast = parse_str::( + r#"fn foo(x: &i64) -> String { format!("{}", x) }"#, + ) + .unwrap(); + assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()); + } + + #[test] + fn test_wrapper_too_many_rust_args_errors() { + // Rust fn has one more arg than the signature declares. + // The old code silently dropped the extra arg; we now reject. + let options = + parse_str::(r#""fn:foo($x as xs:int) as xs:string""#).unwrap(); + let ast = parse_str::( + r#"fn foo(x: &i64, y: &i64) -> String { format!("{}", x) }"#, + ) + .unwrap(); + assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()); + } + + #[test] + fn test_wrapper_arity_mismatch_with_injected_context() { + // Even with context injection, arity is checked correctly: + // signature 0 params + context injected = 1 Rust arg expected, + // but Rust fn has 2. + let options = + parse_str::(r#""fn:foo() as xs:string""#).unwrap(); + let ast = parse_str::( + r#"fn foo(context: &DynamicContext, extra: &i64) -> String { String::new() }"#, + ) + .unwrap(); + assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()); + } } From d82421ce7462910e826b57a592ed92a3636dd552 Mon Sep 17 00:00:00 2001 From: Wietse Date: Tue, 19 May 2026 16:41:43 +0200 Subject: [PATCH 3/3] Run cargo fmt --- xee-xpath-macros/src/wrapper.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/xee-xpath-macros/src/wrapper.rs b/xee-xpath-macros/src/wrapper.rs index 499e501cf..038e7dfa4 100644 --- a/xee-xpath-macros/src/wrapper.rs +++ b/xee-xpath-macros/src/wrapper.rs @@ -82,9 +82,7 @@ fn make_wrapper( let rust_user_count = ast.sig.inputs.len().saturating_sub(adjust); let injected_note = match adjust { 0 => String::new(), - n => format!( - " (after subtracting {n} injected `context`/`interpreter` argument(s))" - ), + n => format!(" (after subtracting {n} injected `context`/`interpreter` argument(s))"), }; bail_spanned!( ast.sig.ident.span() => @@ -213,14 +211,10 @@ mod tests { // signature declares two XPath parameters but the Rust fn only // takes one — used to panic with index-out-of-bounds; now should // surface as a clean syn::Error pointing at the fn name. - let options = parse_str::( - r#""fn:foo($x as xs:int, $y as xs:int) as xs:string""#, - ) - .unwrap(); - let ast = parse_str::( - r#"fn foo(x: &i64) -> String { format!("{}", x) }"#, - ) - .unwrap(); + let options = + parse_str::(r#""fn:foo($x as xs:int, $y as xs:int) as xs:string""#) + .unwrap(); + let ast = parse_str::(r#"fn foo(x: &i64) -> String { format!("{}", x) }"#).unwrap(); assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()); } @@ -230,10 +224,8 @@ mod tests { // The old code silently dropped the extra arg; we now reject. let options = parse_str::(r#""fn:foo($x as xs:int) as xs:string""#).unwrap(); - let ast = parse_str::( - r#"fn foo(x: &i64, y: &i64) -> String { format!("{}", x) }"#, - ) - .unwrap(); + let ast = parse_str::(r#"fn foo(x: &i64, y: &i64) -> String { format!("{}", x) }"#) + .unwrap(); assert_debug_snapshot!(xpath_fn_wrapper(&ast, &options).unwrap_err().to_string()); } @@ -242,8 +234,7 @@ mod tests { // Even with context injection, arity is checked correctly: // signature 0 params + context injected = 1 Rust arg expected, // but Rust fn has 2. - let options = - parse_str::(r#""fn:foo() as xs:string""#).unwrap(); + let options = parse_str::(r#""fn:foo() as xs:string""#).unwrap(); let ast = parse_str::( r#"fn foo(context: &DynamicContext, extra: &i64) -> String { String::new() }"#, )