Skip to content

Fix chained predicates on a filter expression - #143

Merged
zhengchun merged 2 commits into
antchfx:masterfrom
dastrobu:fix/chained-predicate-on-grouped-expression
Sep 25, 2026
Merged

zhengchun merged 2 commits into
antchfx:masterfrom
dastrobu:fix/chained-predicate-on-grouped-expression

Conversation

@dastrobu

Copy link
Copy Markdown
Contributor

parseFilterExpr uses if where the left-recursive grammar in its own comment
needs a loop, so every predicate after the first is silently discarded.
(//a)[1][b] parses to the same AST as (//a)[1].

with <root><table class="t"/><table class="u"/></root>:

expression before want
(//table)[1][false()] 1 node 0
(//table)[1][@class="u"] the class="t" node 0

The second row is the one that bites: a real filter is dropped, so the result is
the wrong node rather than no node, with no error. parseStep already loops;
the un-parenthesized //table[1][@class="u"] goes through it and is correct today.

Nothing in build.go changes. The builder already handled the nested
filterNode; it never received one.

TestChainedPredicateOnGroupedExpression asserts three cases where the predicate
must eliminate the node and three where it must keep one, so it cannot be
satisfied by dropping nodes unconditionally. The two halves that already work were
each covered before (TestPredicates has /empinfo/employee[1][@id=1],
TestPositions has (//book[@category = "web"])[2]) only the combination was
untested.

This changes results for any (...)[a][b] expression that previously ignored
everything after [a]. The old result was incorrect, so this is a fix rather than
a regression; the existing suite passes unchanged.

A predicate chained onto a parenthesized node test is ignored, so the
expression returns whatever the first predicate selected as though the second
predicate had not been written.

    (//employee)[1][false()]     1 node, want 0
    (//employee)[2][false()]     1 node, want 0
    (//employee)[1][@id = "2"]   employee 1, want 0

The third case is the consequential one: a genuine filter is discarded, so the
expression returns the wrong node rather than no node. A caller that chains a
predicate to confirm it selected the node it intended gets no confirmation and
no error.

The un-parenthesized spelling is already covered by TestPredicates
(/empinfo/employee[1][@id=1]) and passes, and a parenthesized node test with a
single predicate is covered by TestPositions and also passes, so only the
combination of the two was untested.

Three cases whose predicate the selected node does satisfy are included so the
test cannot be satisfied by dropping the node unconditionally.
FilterExpr is left-recursive, so a filter expression can carry any number of
predicates. parseFilterExpr consumed at most one, silently discarding the rest:
(//employee)[1][false()] parsed to the same AST as (//employee)[1], and the
second predicate never reached the query builder.

The visible effect is that a chained predicate on a parenthesized node test is
ignored. For an impossible predicate the expression returns a node where it
should return none, and for a real filter it returns the wrong node, which is
worse because there is no error either way.

parseStep already loops for exactly this reason. Matching it here is the whole
fix; nothing in build.go needed to change, since the builder handled the nested
filterNode correctly once the parser produced it.

Verified against xmllint, which agrees on all of:

    (//table)[1][false()]              0
    (//table)[1][@Class="u"]           0
    (//table)[1][@Class="t"]           1
    (//table)[2][@Class="u"]           1
    (//table)[1][true()][@Class="u"]   0
@zhengchun
zhengchun merged commit 5dbec0f into antchfx:master Sep 25, 2026
@dastrobu
dastrobu deleted the fix/chained-predicate-on-grouped-expression branch September 25, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants