From 24565ebbd8b841c4b463fe541ac545b6479978e7 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 2 Mar 2026 22:35:40 -0500 Subject: [PATCH] [bugfix] Fix // followed by reverse axis step being misinterpreted The // abbreviation in XPath expands to /descendant-or-self::node()/. When followed by a reverse axis (preceding, ancestor, etc.), the tree walker incorrectly overwrote the reverse axis with DESCENDANT_SELF_AXIS, destroying the original semantics. For example, $node//preceding::node() behaved like $node/descendant-or-self::node() instead of the correct $node/descendant-or-self::node()/preceding::node(). Fix both the DSLASH and ABSOLUTE_DSLASH handlers to detect reverse axes (constants 0-4) and insert an explicit descendant-or-self::node() step before the reverse axis step, rather than merging them. Closes #691 Co-Authored-By: Claude Opus 4.6 --- .../org/exist/xquery/parser/XQueryTree.g | 15 ++++- .../java/org/exist/xquery/XPathQueryTest.java | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 048ee8e7e85..a4392c13808 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -2360,7 +2360,13 @@ throws PermissionDeniedException, EXistException, XPathException (s.getTest().getType() == Type.ATTRIBUTE && s.getAxis() == Constants.CHILD_AXIS)) // combines descendant-or-self::node()/attribute:* s.setAxis(Constants.DESCENDANT_ATTRIBUTE_AXIS); - else { + else if (s.getAxis() <= Constants.PRECEDING_SIBLING_AXIS) { + // Reverse axis: insert explicit descendant-or-self::node() step + LocationStep descStep = new LocationStep(context, Constants.DESCENDANT_SELF_AXIS, new TypeTest(Type.NODE)); + descStep.setAbbreviated(true); + path.replaceLastExpression(descStep); + path.add(step); + } else { s.setAxis(Constants.DESCENDANT_SELF_AXIS); s.setAbbreviated(true); } @@ -2984,6 +2990,13 @@ throws PermissionDeniedException, EXistException, XPathException rs.setAxis(Constants.DESCENDANT_AXIS); } else if (rs.getAxis() == Constants.SELF_AXIS) { rs.setAxis(Constants.DESCENDANT_SELF_AXIS); + } else if (rs.getAxis() <= Constants.PRECEDING_SIBLING_AXIS) { + // Reverse axis: cannot merge with descendant-or-self, + // insert explicit descendant-or-self::node() step before the reverse axis step + LocationStep descStep = new LocationStep(context, Constants.DESCENDANT_SELF_AXIS, new TypeTest(Type.NODE)); + descStep.setAbbreviated(true); + path.replaceLastExpression(descStep); + path.add(rightStep); } else { rs.setAxis(Constants.DESCENDANT_SELF_AXIS); rs.setAbbreviated(true); diff --git a/exist-core/src/test/java/org/exist/xquery/XPathQueryTest.java b/exist-core/src/test/java/org/exist/xquery/XPathQueryTest.java index e7fe9b350da..1e035a2ec18 100644 --- a/exist-core/src/test/java/org/exist/xquery/XPathQueryTest.java +++ b/exist-core/src/test/java/org/exist/xquery/XPathQueryTest.java @@ -808,6 +808,65 @@ public void precedingAxis() throws XMLDBException { queryResource(service, "siblings.xml", "//a/n[. = '3']/preceding::s", 3); } + /** + * Tests that // followed by a reverse axis correctly expands to + * /descendant-or-self::node()/ + reverse axis, rather than + * collapsing the reverse axis into descendant-or-self. + * + * @see #691 + */ + @Test + public void dslashWithReverseAxis() throws XMLDBException { + final String xml = + "" + + " " + + " 1" + + " 2" + + " " + + " " + + " 3" + + " 4" + + " " + + ""; + + final XQueryService service = + storeXMLStringAndGetQueryService("dslash_reverse.xml", xml); + + // //preceding::b should produce the same count as the expanded form + queryAndAssert(service, + "let $d := doc('/db/test/dslash_reverse.xml') " + + "return count($d//preceding::b) eq count($d/descendant-or-self::node()/preceding::b)", + 1, "//preceding::b count should match expanded form"); + + // //ancestor::a should produce the same count as the expanded form + queryAndAssert(service, + "let $d := doc('/db/test/dslash_reverse.xml') " + + "return count($d//ancestor::a) eq count($d/descendant-or-self::node()/ancestor::a)", + 1, "//ancestor::a count should match expanded form"); + + // Note: //preceding-sibling::b skipped due to pre-existing NPE in + // NewArrayNodeSet.selectPrecedingSiblings when evaluating preceding-sibling + // on descendant-or-self::node() context (affects both abbreviated and expanded forms) + + // //ancestor-or-self::a should produce the same count as the expanded form + queryAndAssert(service, + "let $d := doc('/db/test/dslash_reverse.xml') " + + "return count($d//ancestor-or-self::a) eq count($d/descendant-or-self::node()/ancestor-or-self::a)", + 1, "//ancestor-or-self::a count should match expanded form"); + + // //parent::a should produce the same count as the expanded form + queryAndAssert(service, + "let $d := doc('/db/test/dslash_reverse.xml') " + + "return count($d//parent::a) eq count($d/descendant-or-self::node()/parent::a)", + 1, "//parent::a count should match expanded form"); + + // Relative path: $node//preceding::b should match expanded form + queryAndAssert(service, + "let $node := doc('/db/test/dslash_reverse.xml')/root/a[2] " + + "return count($node//preceding::b) eq count($node/descendant-or-self::node()/preceding::b)", + 1, "$node//preceding::b count should match expanded form"); + } + @Test public void position() throws XMLDBException, IOException, SAXException {