diff --git a/benchmark/xpath.yaml b/benchmark/xpath.yaml
index d6e970eb..072e3160 100644
--- a/benchmark/xpath.yaml
+++ b/benchmark/xpath.yaml
@@ -24,9 +24,10 @@ contexts:
prelude: |
require 'rexml/document'
- DEPTH = 100
+ DEPTH = 30
xml = '' * DEPTH + '' * DEPTH
doc = REXML::Document.new(xml)
benchmark:
"REXML::XPath.match(REXML::Document.new(xml), 'a//a')" : REXML::XPath.match(doc, "a//a")
+ "REXML::XPath.match(REXML::Document.new(xml), '//a//a')" : REXML::XPath.match(doc, "//a//a")
diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb
index 64c8846a..c5d420ce 100644
--- a/lib/rexml/xpath_parser.rb
+++ b/lib/rexml/xpath_parser.rb
@@ -462,21 +462,20 @@ def step(path_stack, any_type: :element, order: :forward)
if nodesets.size == 1
ordered_nodeset = nodesets[0]
else
+ seen = {}.compare_by_identity
raw_nodes = []
nodesets.each do |nodeset|
nodeset.each do |node|
- if node.respond_to?(:raw_node)
- raw_nodes << node.raw_node
- else
- raw_nodes << node
- end
+ raw_node = node.respond_to?(:raw_node) ? node.raw_node : node
+ next if seen.key?(raw_node)
+ seen[raw_node] = true
+ raw_nodes << raw_node
end
end
ordered_nodeset = sort(raw_nodes, order)
end
new_nodeset = []
ordered_nodeset.each do |node|
- # TODO: Remove duplicated
new_nodeset << XPathNode.new(node, position: new_nodeset.size + 1)
end
new_nodeset
diff --git a/test/xpath/test_base.rb b/test/xpath/test_base.rb
index 764171ab..1015014f 100644
--- a/test/xpath/test_base.rb
+++ b/test/xpath/test_base.rb
@@ -1276,5 +1276,109 @@ def test_match_with_deprecated_usage
ensure
$VERBOSE = verbose
end
+
+ def test_descendant_no_duplicates
+ doc = <<-EOT
+
+
+
+
+
+
+ EOT
+
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a//a")
+ assert_equal(["1", "2", "3"], result.map { |e| e.attributes["id"] })
+ end
+
+ def test_descendant_document_order
+ doc = <<-EOT
+
+
+
+
+
+
+
+ EOT
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a//a")
+ assert_equal(["1", "2", "3"], result.map { |e| e.attributes["id"] })
+ end
+
+ def test_descendant_ancestor_descendant_overlap
+ doc = <<-EOT
+
+
+
+
+
+
+
+ EOT
+
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a//b")
+ assert_equal(["b1"], result.map { |e| e.attributes["id"] })
+ end
+
+ def test_descendant_sibling_branches
+ doc = <<-EOT
+
+
+
+
+
+
+
+
+ EOT
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a//b")
+ assert_equal(["b1", "b2"], result.map { |e| e.attributes["id"] })
+ end
+
+ def test_descendant_document_order_with_shared_subtree
+ doc = <<-EOT
+
+
+
+
+
+
+
+
+ EOT
+
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a//b")
+ assert_equal(["b1", "b2"], result.map { |e| e.attributes["id"] })
+ end
+
+ def test_descendant_axis_on_leaf_element
+ doc = Document.new("")
+ result = XPath.match(doc, "//b/descendant::node()")
+ assert_equal([], result)
+ end
+
+ def test_descendant_axis_position_predicate_per_context_node
+ doc = <<-EOT
+
+
+
+
+
+
+
+
+
+
+ EOT
+
+ xmldoc = Document.new(doc)
+ result = XPath.match(xmldoc, "//a/descendant::b[1]")
+ assert_equal(["b1", "b2"], result.map { |e| e.attributes["id"] })
+ end
end
end