From 44bb0e12f1dda0d7523be0b56871c659bdc54257 Mon Sep 17 00:00:00 2001 From: boukeversteegh Date: Fri, 17 Apr 2026 10:29:03 +0200 Subject: [PATCH 1/2] Fix predicate numeric probe erroring on multi-item sequences (#147) `pop_is_numeric` used `atomized_option`, which raises XPTY0004 on any sequence of length > 1. That turned predicates like `a[b]` into errors whenever `` had more than one `` child, instead of falling through to EBV as the XPath spec requires. Return `false` for empty or multi-item atomized sequences so the EBV path handles them. Updates one snapshot (`(1,2,3)[(2,3)]` now fails with FORG0006 from EBV instead of XPTY0004) and adds a regression test for `//a[b]` against multi-child XML. Co-Authored-By: Claude Opus 4.7 (1M context) --- vendor/xpath-tests/filters | 5 ----- xee-interpreter/src/interpreter/interpret.rs | 12 +++++++----- ...h__sequence_predicate_sequence_too_long.snap | 2 +- xee-xpath/tests/xpath.rs | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/vendor/xpath-tests/filters b/vendor/xpath-tests/filters index d2fdb94f..01456e24 100644 --- a/vendor/xpath-tests/filters +++ b/vendor/xpath-tests/filters @@ -1991,11 +1991,6 @@ K2-NodeTest-19 = prod-PathExpr = prod-PositionalVar = prod-Predicate -K-FilterExpr-38 -K-FilterExpr-40 -K-FilterExpr-92 -K-FilterExpr-93 -K-FilterExpr-94 = prod-QuantifiedExpr = prod-ReturnClause = prod-SchemaImport diff --git a/xee-interpreter/src/interpreter/interpret.rs b/xee-interpreter/src/interpreter/interpret.rs index 7e2a34a5..436f2664 100644 --- a/xee-interpreter/src/interpreter/interpret.rs +++ b/xee-interpreter/src/interpreter/interpret.rs @@ -1045,11 +1045,13 @@ impl<'a> Interpreter<'a> { fn pop_is_numeric(&mut self) -> error::Result { let value = self.state.pop()?; - let a = value.atomized_option(self.state.xot())?; - if let Some(a) = a { - Ok(a.is_numeric()) - } else { - Ok(false) + // Only a single atomized value can be numeric; an empty or multi-item + // sequence is not numeric (and must not error — the caller falls back + // to EBV for those). + let mut atomized = value.atomized(self.state.xot()); + match (atomized.next(), atomized.next()) { + (Some(only), None) => Ok(only?.is_numeric()), + _ => Ok(false), } } diff --git a/xee-xpath/tests/snapshots/xpath__sequence_predicate_sequence_too_long.snap b/xee-xpath/tests/snapshots/xpath__sequence_predicate_sequence_too_long.snap index a275f69b..2fce4c22 100644 --- a/xee-xpath/tests/snapshots/xpath__sequence_predicate_sequence_too_long.snap +++ b/xee-xpath/tests/snapshots/xpath__sequence_predicate_sequence_too_long.snap @@ -4,7 +4,7 @@ expression: "run(\"(1, 2, 3)[(2, 3)]\")" --- Err( SpannedError { - error: XPTY0004, + error: FORG0006, span: Some( SourceSpan( 10, diff --git a/xee-xpath/tests/xpath.rs b/xee-xpath/tests/xpath.rs index 98d560fc..481c8acf 100644 --- a/xee-xpath/tests/xpath.rs +++ b/xee-xpath/tests/xpath.rs @@ -395,6 +395,23 @@ fn test_sequence_predicate_sequence_empty() { assert_debug_snapshot!(run("(1, 2, 3)[()]")); } +#[test] +fn test_node_predicate_with_multiple_children() -> error::Result<()> { + // Regression: `a[b]` must match even when an `` has multiple `` + // children. The inner sequence should be treated as a node sequence and + // evaluated via EBV, not rejected by the numeric-predicate probe. + assert_nodes( + r#"123"#, + "//a[b]", + |xot, root| { + let root_el = xot.document_element(root).unwrap(); + let a1 = xot.first_child(root_el).unwrap(); + let a2 = xot.next_sibling(a1).unwrap(); + vec![a1, a2] + }, + ) +} + #[test] fn test_child_axis_step1() -> error::Result<()> { assert_nodes(r#""#, "doc/*", |xot, root| { From 120fd8fb5b85d78392435b1c38eaca27e7da07a1 Mon Sep 17 00:00:00 2001 From: boukeversteegh Date: Fri, 17 Apr 2026 16:37:22 +0200 Subject: [PATCH 2/2] Fix clippy lints for Rust 1.95 (`-D warnings`) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four lints newly promoted in Rust 1.95.0 started failing CI: - `interpret.rs`: collapsible_match — merge nested `if` into a guarded match arm on `xot::Value::Text(text) if text.get().is_empty()`. - `library/map.rs` (x2): useless_conversion — drop `.into_iter()` on values already implementing `IntoIterator` inside `Vec::extend`. - `xslt-ast/whitespace.rs`: collapsible_match — same pattern, merge nested `if` into the outer `Value::Text(text)` arm's guard. Mechanical clippy autofix equivalents; no behavior change. Verified locally against rustc 1.95.0 with `cargo clippy --all-targets --all-features -- -D warnings` and `cargo test --workspace`. Co-Authored-By: Claude Opus 4.7 (1M context) --- xee-interpreter/src/interpreter/interpret.rs | 12 +++++------- xee-interpreter/src/library/map.rs | 4 ++-- xee-xslt-ast/src/whitespace.rs | 9 ++++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/xee-interpreter/src/interpreter/interpret.rs b/xee-interpreter/src/interpreter/interpret.rs index 436f2664..d9d7703b 100644 --- a/xee-interpreter/src/interpreter/interpret.rs +++ b/xee-interpreter/src/interpreter/interpret.rs @@ -1132,13 +1132,11 @@ impl<'a> Interpreter<'a> { "Appending a whole document is not supported yet", ))); } - xot::Value::Text(text) => { - // zero length text nodes are skipped - // Can this even exist, or does Xot not have - // them anyway? - if text.get().is_empty() { - continue; - } + // zero length text nodes are skipped + // Can this even exist, or does Xot not have + // them anyway? + xot::Value::Text(text) if text.get().is_empty() => { + continue; } _ => {} } diff --git a/xee-interpreter/src/library/map.rs b/xee-interpreter/src/library/map.rs index 5d1126ef..e3cf087a 100644 --- a/xee-interpreter/src/library/map.rs +++ b/xee-interpreter/src/library/map.rs @@ -162,7 +162,7 @@ fn find_helper( function::Function::Array(array) => { for entry in array.iter() { let found = find_helper(entry, key.clone())?; - result.extend(found.into_iter()) + result.extend(found) } } function::Function::Map(map) => { @@ -171,7 +171,7 @@ fn find_helper( result.push(v.clone()); } let found = find_helper(v, key.clone())?; - result.extend(found.into_iter()) + result.extend(found) } } _ => {} diff --git a/xee-xslt-ast/src/whitespace.rs b/xee-xslt-ast/src/whitespace.rs index 69f7216a..626ab519 100644 --- a/xee-xslt-ast/src/whitespace.rs +++ b/xee-xslt-ast/src/whitespace.rs @@ -14,12 +14,11 @@ pub(crate) fn strip_whitespace(xot: &mut Xot, names: &Names, node: Node) { for edge in xot.traverse(node) { match edge { NodeEdge::Start(node) => match xot.value(node) { - Value::Text(text) => { + Value::Text(text) if is_xml_whitespace(text.get()) - && !is_xml_space_preserve(xot, names, node, &xml_space_preserve) - { - to_remove.push(node); - } + && !is_xml_space_preserve(xot, names, node, &xml_space_preserve) => + { + to_remove.push(node); } Value::Element(_) => { if let Some(xml_space) = xot.attributes(node).get(xot.xml_space_name()) {