Skip to content

Commit f9a7d47

Browse files
committed
Fix reviewed points
1 parent b7d96b3 commit f9a7d47

2 files changed

Lines changed: 71 additions & 10 deletions

File tree

lib/rexml/xpath_parser.rb

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -345,16 +345,42 @@ def position_dependency(predicate_expr)
345345
# [number], [position()=number], [position() < number], [position() > number]
346346
return :simple if position_operation(predicate_expr)
347347

348-
# expressions that return number.
349-
return :complex if %i[div mod mult plus minus neg].include?(predicate_expr[0])
350-
return :complex if predicate_expr[0] == :function && %w[number ceiling round floor string-length sum count].include?(predicate_expr[1])
351-
# Numeric literal including Integer and Float: [2] means [position() = 2]
352-
return :complex if predicate_expr[0] == :literal && Numeric === predicate_expr[1]
353-
# A variable could resolve to a number at runtime: [$n] means [position() = $n].
354-
return :complex if predicate_expr[0] == :variable
355-
356348
# expressions that contain position-dependent functions
357349
return :complex if calls_position_dependent_function?(predicate_expr)
350+
351+
# Even if expression is followed by path steps, the analysis of
352+
# position dependency is the same as the expression itself.
353+
case predicate_expr[0]
354+
when :union, :or, :and, :eq, :neq, :lt, :lteq, :gt, :gteq, :not
355+
# Expressions that doesn't evaluate to number is position independent
356+
# if it doesn't contain position-dependent functions.
357+
nil
358+
when :div, :mod, :mult, :plus, :minus, :neg
359+
# expressions that return number. eg. `[position() = @attr + 1]`
360+
:complex
361+
when :literal
362+
# Integer literal is handled in `position_operation(predicate_expr)`.
363+
# We treat this as complex(no-optimization) because this is not a normal case
364+
# eg. `/foo["hi"]` `/bar[true]` `/baz[3.14]`
365+
:complex
366+
when :variable
367+
# A variable could resolve to a number at runtime.
368+
# It's possible to optimize this by checking the actual value of the variable.
369+
:complex
370+
when :function
371+
# functions that return number is position dependent. eg. `[position() = string-length(@attr)]`
372+
%w[number ceiling round floor string-length sum count].include?(predicate_expr[1]) ? :complex : nil
373+
when :group
374+
position_dependency(predicate_expr[1])
375+
when :descendant, :descendant_or_self, :ancestor, :ancestor_or_self,
376+
:following, :following_sibling, :preceding, :preceding_sibling,
377+
:document, :child, :self, :parent, :attribute, :namespace
378+
# paths are position independent. `foo[path[1]]` doesn't depend on the position of `foo`
379+
nil
380+
else
381+
# Every other unhandled expressions are treated as complex for safety
382+
:complex
383+
end
358384
end
359385

360386
# Recursively checks if the expression contains position-dependent functions such as position() or last()
@@ -514,15 +540,14 @@ def non_optimized_nodesets_select(nodesets, tester, selector)
514540
seen.to_a
515541
else
516542
operator, value = selector
517-
nodes = nodesets.flatten
518543
nodes =
519544
case operator
520545
when :==
521546
nodesets.map {|nodeset| nodeset[value - 1] if value >= 1 }.compact
522547
when :<
523548
nodesets.map {|nodeset| nodeset[0...value - 1] if value >= 1 }.compact.flatten
524549
when :>
525-
nodesets.map {|nodeset| value < 0 ? nodeset : nodeset[value..-1] }.flatten
550+
nodesets.map {|nodeset| value < 0 ? nodeset : nodeset.drop(value) }.flatten
526551
end
527552
seen = Set.new.compare_by_identity
528553
nodes.each {|node| seen << node }

test/xpath/test_predicate.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,42 @@ def test_predicate_variable_as_position
103103
assert_equal(["b"], parser.parse("/r/*[$x]", doc).map(&:name))
104104
end
105105

106+
def test_predicate_out_of_range_position
107+
doc = REXML::Document.new("<r><a/><b/><c/><d/></r>")
108+
parser = REXML::XPathParser.new
109+
base = '/r/*'
110+
assert_equal([], parser.parse("#{base}[-1]", doc).map(&:name))
111+
assert_equal([], parser.parse("#{base}[0]", doc).map(&:name))
112+
assert_equal([], parser.parse("#{base}[5]", doc).map(&:name))
113+
assert_equal([], parser.parse("#{base}[position()>5]", doc).map(&:name))
114+
assert_equal([], parser.parse("#{base}[position()<-1]", doc).map(&:name))
115+
assert_equal(%w[a b c d], parser.parse("#{base}[position()>-1]", doc).map(&:name))
116+
assert_equal(%w[a b c d], parser.parse("#{base}[position()<10]", doc).map(&:name))
117+
118+
# non-optimizable case
119+
base_no_opt = '/r/*[position()!=name()]'
120+
assert_equal([], parser.parse("#{base_no_opt}[-1]", doc).map(&:name))
121+
assert_equal([], parser.parse("#{base_no_opt}[0]", doc).map(&:name))
122+
assert_equal([], parser.parse("#{base_no_opt}[5]", doc).map(&:name))
123+
assert_equal([], parser.parse("#{base_no_opt}[position()>5]", doc).map(&:name))
124+
assert_equal([], parser.parse("#{base_no_opt}[position()<-1]", doc).map(&:name))
125+
assert_equal(%w[a b c d], parser.parse("#{base_no_opt}[position()>-1]", doc).map(&:name))
126+
assert_equal(%w[a b c d], parser.parse("#{base_no_opt}[position()<10]", doc).map(&:name))
127+
end
128+
129+
def test_predicate_parenthesized_position
130+
doc = REXML::Document.new("<r><a/><b/><c/><d/></r>")
131+
parser = REXML::XPathParser.new
132+
assert_equal(["b"], parser.parse("/r/*[(2)]", doc).map(&:name))
133+
end
134+
135+
def test_position_dependent_function_predicates
136+
doc = REXML::Document.new("<r><a/><b/><c/><d/></r>")
137+
parser = REXML::XPathParser.new
138+
assert_equal(["b"], parser.parse("/r/*['2'=string(-(0 - position())*1)]", doc).map(&:name))
139+
assert_equal(%w[a b c d], parser.parse("/r/*['4'=string(-(0 - last())*1)]", doc).map(&:name))
140+
end
141+
106142
def test_get_no_siblings_terminal_nodes
107143
source = <<-XML
108144
<a>

0 commit comments

Comments
 (0)