From 02c01b0b4051e7edd1bf40f3d595cc9143936aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Sun, 9 Nov 2025 16:48:48 +0100 Subject: [PATCH] Fix chained predicates on ancestor axis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The XPath expression with chained predicates on the ancestor axis was not working correctly. For example: //*[@itemprop="author"][ancestor::*[@itemscope][1][@itemtype="Comment"]] This expression should find all elements with @itemprop="author" whose first @itemscope ancestor has @itemtype="Comment". However, it was returning 0 results instead of the expected elements. Root cause: 1. ancestorQuery.table (deduplication table) was persisting across Evaluate() calls, preventing ancestors from being found for subsequent input contexts. 2. filterQuery.positmap (position map) was not being reset during Evaluate(), causing position tracking to be incorrect when the same predicate query was reused for multiple input nodes. Fix: - Reset ancestorQuery.table to nil in Evaluate() to ensure clean state for each evaluation - Reset filterQuery.positmap to nil in Evaluate() to ensure clean state for each evaluation These changes ensure that when a predicate query is evaluated multiple times (once for each candidate node), the internal state is properly reset, allowing the query to work correctly for each evaluation. 💁 Unit test by Mislav 🤖 Implementation by [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- query.go | 4 ++++ xpath_axes_test.go | 22 ++++++++++++++++++++++ xpath_test.go | 19 ++++++++++++++++--- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/query.go b/query.go index 81d65af..2a71734 100644 --- a/query.go +++ b/query.go @@ -201,6 +201,8 @@ func (a *ancestorQuery) Select(t iterator) NodeNavigator { func (a *ancestorQuery) Evaluate(t iterator) interface{} { a.Input.Evaluate(t) a.iterator = nil + // Reset the table when re-evaluating to ensure clean state + a.table = nil return a } @@ -829,6 +831,8 @@ func (f *filterQuery) Select(t iterator) NodeNavigator { func (f *filterQuery) Evaluate(t iterator) interface{} { f.Input.Evaluate(t) + // Reset the position map when re-evaluating to ensure clean state + f.positmap = nil return f } diff --git a/xpath_axes_test.go b/xpath_axes_test.go index 44475ab..4969385 100644 --- a/xpath_axes_test.go +++ b/xpath_axes_test.go @@ -65,6 +65,28 @@ func Test_ancestor_predicate(t *testing.T) { test_xpath_elements(t, doc, `//span/ancestor::section[2]`, 4, 9) } +func Test_ancestor_predicate_chain(t *testing.T) { + doc := createElement(0, "", + createElement(1, "html", + createElementAttr(2, "body", map[string]string{"itemscope": "", "itemtype": "Article"}, + createElement(3, "section", + createElementAttr(4, "span", map[string]string{"itemprop": "author"}), + createElementAttr(5, "div", map[string]string{"itemscope": "", "itemtype": "Comment"}, + createElementAttr(6, "span", map[string]string{"itemprop": "author"}), + createElement(7, "div", + createElementAttr(8, "span", map[string]string{"itemprop": "author"}), + ), + ), + ), + ), + ), + ) + + // Find elements marked as "author" property whose closest "itemscope" ancestor is of "Comment" type. + // This should find "span" elements on lines 6 and 8, but not line 4 since that one is under "Article". + test_xpath_elements(t, doc, `//*[@itemprop="author"][ancestor::*[@itemscope][1][@itemtype="Comment"]]`, 6, 8) +} + func Test_ancestor_or_self(t *testing.T) { // Expected the value is [2, 3, 8, 13], but got [3, 2, 8, 13] test_xpath_elements(t, employee_example, `//employee/ancestor-or-self::*`, 3, 2, 8, 13) diff --git a/xpath_test.go b/xpath_test.go index 6d7de07..4d107f8 100644 --- a/xpath_test.go +++ b/xpath_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math" + "reflect" "sort" "strings" "testing" @@ -41,10 +42,14 @@ func (t testQuery) Properties() queryProp { func test_xpath_elements(t *testing.T, root *TNode, expr string, expected ...int) { result := selectNodes(root, expr) - assertEqual(t, len(expected), len(result)) - for i := 0; i < len(expected); i++ { - assertEqual(t, expected[i], result[i].lines) + var gotLines []int + for i := 0; i < len(result); i++ { + gotLines = append(gotLines, result[i].lines) + } + + if !reflect.DeepEqual(gotLines, expected) { + t.Fatalf("expected lines %+v, got %+v", expected, gotLines) } } @@ -587,6 +592,14 @@ func (n *TNode) getAttribute(key string) string { return "" } +func createElementAttr(line int, name string, attrs map[string]string, children ...*TNode) *TNode { + el := createElement(line, name, children...) + for k, v := range attrs { + el.addAttribute(k, v) + } + return el +} + func createElement(line int, name string, children ...*TNode) *TNode { nodeType := ElementNode if name == "" {