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