mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
[ruby/rexml] xpath: fix a bug for equality or relational expressions
GitHub: fix #17 There is a bug when they are used against node set. They should return boolean value but they returned node set. Reported by Mirko Budszuhn. Thanks!!! https://github.com/ruby/rexml/commit/a02bf38440
This commit is contained in:
parent
c46ba8e9a3
commit
6ef8294397
4 changed files with 178 additions and 114 deletions
|
@ -1,33 +0,0 @@
|
||||||
# frozen_string_literal: false
|
|
||||||
module REXML
|
|
||||||
class SyncEnumerator
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
# Creates a new SyncEnumerator which enumerates rows of given
|
|
||||||
# Enumerable objects.
|
|
||||||
def initialize(*enums)
|
|
||||||
@gens = enums
|
|
||||||
@length = @gens.collect {|x| x.size }.max
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the number of enumerated Enumerable objects, i.e. the size
|
|
||||||
# of each row.
|
|
||||||
def size
|
|
||||||
@gens.size
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the number of enumerated Enumerable objects, i.e. the size
|
|
||||||
# of each row.
|
|
||||||
def length
|
|
||||||
@gens.length
|
|
||||||
end
|
|
||||||
|
|
||||||
# Enumerates rows of the Enumerable objects.
|
|
||||||
def each
|
|
||||||
@length.times {|i|
|
|
||||||
yield @gens.collect {|x| x[i]}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,7 +5,6 @@ require "pp"
|
||||||
require_relative 'namespace'
|
require_relative 'namespace'
|
||||||
require_relative 'xmltokens'
|
require_relative 'xmltokens'
|
||||||
require_relative 'attribute'
|
require_relative 'attribute'
|
||||||
require_relative 'syncenumerator'
|
|
||||||
require_relative 'parsers/xpathparser'
|
require_relative 'parsers/xpathparser'
|
||||||
|
|
||||||
class Object
|
class Object
|
||||||
|
@ -141,7 +140,7 @@ module REXML
|
||||||
when Array # nodeset
|
when Array # nodeset
|
||||||
unnode(result)
|
unnode(result)
|
||||||
else
|
else
|
||||||
result
|
[result]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -341,26 +340,24 @@ module REXML
|
||||||
var_name = path_stack.shift
|
var_name = path_stack.shift
|
||||||
return [@variables[var_name]]
|
return [@variables[var_name]]
|
||||||
|
|
||||||
# :and, :or, :eq, :neq, :lt, :lteq, :gt, :gteq
|
when :eq, :neq, :lt, :lteq, :gt, :gteq
|
||||||
# TODO: Special case for :or and :and -- not evaluate the right
|
|
||||||
# operand if the left alone determines result (i.e. is true for
|
|
||||||
# :or and false for :and).
|
|
||||||
when :eq, :neq, :lt, :lteq, :gt, :gteq, :or
|
|
||||||
left = expr( path_stack.shift, nodeset.dup, context )
|
left = expr( path_stack.shift, nodeset.dup, context )
|
||||||
right = expr( path_stack.shift, nodeset.dup, context )
|
right = expr( path_stack.shift, nodeset.dup, context )
|
||||||
res = equality_relational_compare( left, op, right )
|
res = equality_relational_compare( left, op, right )
|
||||||
trace(op, left, right, res) if @debug
|
trace(op, left, right, res) if @debug
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
when :or
|
||||||
|
left = expr(path_stack.shift, nodeset.dup, context)
|
||||||
|
return true if Functions.boolean(left)
|
||||||
|
right = expr(path_stack.shift, nodeset.dup, context)
|
||||||
|
return Functions.boolean(right)
|
||||||
|
|
||||||
when :and
|
when :and
|
||||||
left = expr( path_stack.shift, nodeset.dup, context )
|
left = expr(path_stack.shift, nodeset.dup, context)
|
||||||
return [] unless left
|
return false unless Functions.boolean(left)
|
||||||
if left.respond_to?(:inject) and !left.inject(false) {|a,b| a | b}
|
right = expr(path_stack.shift, nodeset.dup, context)
|
||||||
return []
|
return Functions.boolean(right)
|
||||||
end
|
|
||||||
right = expr( path_stack.shift, nodeset.dup, context )
|
|
||||||
res = equality_relational_compare( left, op, right )
|
|
||||||
return res
|
|
||||||
|
|
||||||
when :div, :mod, :mult, :plus, :minus
|
when :div, :mod, :mult, :plus, :minus
|
||||||
left = expr(path_stack.shift, nodeset, context)
|
left = expr(path_stack.shift, nodeset, context)
|
||||||
|
@ -397,31 +394,34 @@ module REXML
|
||||||
when :function
|
when :function
|
||||||
func_name = path_stack.shift.tr('-','_')
|
func_name = path_stack.shift.tr('-','_')
|
||||||
arguments = path_stack.shift
|
arguments = path_stack.shift
|
||||||
subcontext = context ? nil : { :size => nodeset.size }
|
|
||||||
|
|
||||||
res = []
|
if nodeset.size != 1
|
||||||
cont = context
|
message = "[BUG] Node set size must be 1 for function call: "
|
||||||
nodeset.each_with_index do |node, i|
|
message += "<#{func_name}>: <#{nodeset.inspect}>: "
|
||||||
if subcontext
|
message += "<#{arguments.inspect}>"
|
||||||
if node.is_a?(XPathNode)
|
raise message
|
||||||
subcontext[:node] = node.raw_node
|
end
|
||||||
subcontext[:index] = node.position
|
|
||||||
|
node = nodeset.first
|
||||||
|
if context
|
||||||
|
target_context = context
|
||||||
else
|
else
|
||||||
subcontext[:node] = node
|
target_context = {:size => nodeset.size}
|
||||||
subcontext[:index] = i
|
if node.is_a?(XPathNode)
|
||||||
|
target_context[:node] = node.raw_node
|
||||||
|
target_context[:index] = node.position
|
||||||
|
else
|
||||||
|
target_context[:node] = node
|
||||||
|
target_context[:index] = 1
|
||||||
end
|
end
|
||||||
cont = subcontext
|
|
||||||
end
|
end
|
||||||
arg_clone = arguments.dclone
|
args = arguments.dclone.collect do |arg|
|
||||||
args = arg_clone.collect do |arg|
|
result = expr(arg, nodeset, target_context)
|
||||||
result = expr( arg, [node], cont )
|
|
||||||
result = unnode(result) if result.is_a?(Array)
|
result = unnode(result) if result.is_a?(Array)
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
Functions.context = cont
|
Functions.context = target_context
|
||||||
res << Functions.send( func_name, *args )
|
return Functions.send(func_name, *args)
|
||||||
end
|
|
||||||
return res
|
|
||||||
|
|
||||||
else
|
else
|
||||||
raise "[BUG] Unexpected path: <#{op.inspect}>: <#{path_stack.inspect}>"
|
raise "[BUG] Unexpected path: <#{op.inspect}>: <#{path_stack.inspect}>"
|
||||||
|
@ -806,31 +806,28 @@ module REXML
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def equality_relational_compare( set1, op, set2 )
|
def equality_relational_compare(set1, op, set2)
|
||||||
set1 = unnode(set1) if set1.is_a?(Array)
|
set1 = unnode(set1) if set1.is_a?(Array)
|
||||||
set2 = unnode(set2) if set2.is_a?(Array)
|
set2 = unnode(set2) if set2.is_a?(Array)
|
||||||
|
|
||||||
if set1.kind_of? Array and set2.kind_of? Array
|
if set1.kind_of? Array and set2.kind_of? Array
|
||||||
if set1.size == 0 or set2.size == 0
|
# If both objects to be compared are node-sets, then the
|
||||||
nd = set1.size==0 ? set2 : set1
|
# comparison will be true if and only if there is a node in the
|
||||||
rv = nd.collect { |il| compare( il, op, nil ) }
|
# first node-set and a node in the second node-set such that the
|
||||||
return rv
|
# result of performing the comparison on the string-values of
|
||||||
else
|
# the two nodes is true.
|
||||||
res = []
|
set1.product(set2).any? do |node1, node2|
|
||||||
SyncEnumerator.new( set1, set2 ).each { |i1, i2|
|
node_string1 = Functions.string(node1)
|
||||||
i1 = norm( i1 )
|
node_string2 = Functions.string(node2)
|
||||||
i2 = norm( i2 )
|
compare(node_string1, op, node_string2)
|
||||||
res << compare( i1, op, i2 )
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
elsif set1.kind_of? Array or set2.kind_of? Array
|
||||||
# If one is nodeset and other is number, compare number to each item
|
# If one is nodeset and other is number, compare number to each item
|
||||||
# in nodeset s.t. number op number(string(item))
|
# in nodeset s.t. number op number(string(item))
|
||||||
# If one is nodeset and other is string, compare string to each item
|
# If one is nodeset and other is string, compare string to each item
|
||||||
# in nodeset s.t. string op string(item)
|
# in nodeset s.t. string op string(item)
|
||||||
# If one is nodeset and other is boolean, compare boolean to each item
|
# If one is nodeset and other is boolean, compare boolean to each item
|
||||||
# in nodeset s.t. boolean op boolean(item)
|
# in nodeset s.t. boolean op boolean(item)
|
||||||
if set1.kind_of? Array or set2.kind_of? Array
|
|
||||||
if set1.kind_of? Array
|
if set1.kind_of? Array
|
||||||
a = set1
|
a = set1
|
||||||
b = set2
|
b = set2
|
||||||
|
@ -841,15 +838,23 @@ module REXML
|
||||||
|
|
||||||
case b
|
case b
|
||||||
when true, false
|
when true, false
|
||||||
return unnode(a) {|v| compare( Functions::boolean(v), op, b ) }
|
each_unnode(a).any? do |unnoded|
|
||||||
|
compare(Functions.boolean(unnoded), op, b)
|
||||||
|
end
|
||||||
when Numeric
|
when Numeric
|
||||||
return unnode(a) {|v| compare( Functions::number(v), op, b )}
|
each_unnode(a).any? do |unnoded|
|
||||||
when /^\d+(\.\d+)?$/
|
compare(Functions.number(unnoded), op, b)
|
||||||
b = Functions::number( b )
|
end
|
||||||
return unnode(a) {|v| compare( Functions::number(v), op, b )}
|
when /\A\d+(\.\d+)?\z/
|
||||||
|
b = Functions.number(b)
|
||||||
|
each_unnode(a).any? do |unnoded|
|
||||||
|
compare(Functions.number(unnoded), op, b)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
b = Functions::string( b )
|
b = Functions::string(b)
|
||||||
return unnode(a) { |v| compare( Functions::string(v), op, b ) }
|
each_unnode(a).any? do |unnoded|
|
||||||
|
compare(Functions::string(unnoded), op, b)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# If neither is nodeset,
|
# If neither is nodeset,
|
||||||
|
@ -880,13 +885,12 @@ module REXML
|
||||||
set2 = Functions::number( set2 )
|
set2 = Functions::number( set2 )
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return compare( set1, op, set2 )
|
compare( set1, op, set2 )
|
||||||
end
|
end
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def compare a, op, b
|
def compare(a, operator, b)
|
||||||
case op
|
case operator
|
||||||
when :eq
|
when :eq
|
||||||
a == b
|
a == b
|
||||||
when :neq
|
when :neq
|
||||||
|
@ -899,22 +903,27 @@ module REXML
|
||||||
a > b
|
a > b
|
||||||
when :gteq
|
when :gteq
|
||||||
a >= b
|
a >= b
|
||||||
when :and
|
|
||||||
a and b
|
|
||||||
when :or
|
|
||||||
a or b
|
|
||||||
else
|
else
|
||||||
false
|
message = "[BUG] Unexpected compare operator: " +
|
||||||
|
"<#{operator.inspect}>: <#{a.inspect}>: <#{b.inspect}>"
|
||||||
|
raise message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unnode(nodeset)
|
def each_unnode(nodeset)
|
||||||
nodeset.collect do |node|
|
return to_enum(__method__, nodeset) unless block_given?
|
||||||
|
nodeset.each do |node|
|
||||||
if node.is_a?(XPathNode)
|
if node.is_a?(XPathNode)
|
||||||
unnoded = node.raw_node
|
unnoded = node.raw_node
|
||||||
else
|
else
|
||||||
unnoded = node
|
unnoded = node
|
||||||
end
|
end
|
||||||
|
yield(unnoded)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unnode(nodeset)
|
||||||
|
each_unnode(nodeset).collect do |unnoded|
|
||||||
unnoded = yield(unnoded) if block_given?
|
unnoded = yield(unnoded) if block_given?
|
||||||
unnoded
|
unnoded
|
||||||
end
|
end
|
||||||
|
|
|
@ -369,11 +369,15 @@ module REXMLTests
|
||||||
assert_equal 2, c
|
assert_equal 2, c
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match(xpath)
|
||||||
|
XPath.match(@@doc, xpath).collect(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
def test_grouping
|
def test_grouping
|
||||||
t = XPath.first( @@doc, "a/d/*[name()='d' and (name()='f' or name()='q')]" )
|
assert_equal([],
|
||||||
assert_nil t
|
match("a/d/*[name()='d' and (name()='f' or name()='q')]"))
|
||||||
t = XPath.first( @@doc, "a/d/*[(name()='d' and name()='f') or name()='q']" )
|
assert_equal(["<q id='19'/>"],
|
||||||
assert_equal 'q', t.name
|
match("a/d/*[(name()='d' and name()='f') or name()='q']"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_preceding
|
def test_preceding
|
||||||
|
|
84
test/rexml/xpath/test_node_set.rb
Normal file
84
test/rexml/xpath/test_node_set.rb
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# frozen_string_literal: false
|
||||||
|
|
||||||
|
require_relative "../rexml_test_utils"
|
||||||
|
|
||||||
|
require "rexml/document"
|
||||||
|
|
||||||
|
module REXMLTests
|
||||||
|
class TestXPathNodeSet < Test::Unit::TestCase
|
||||||
|
def match(xml, xpath)
|
||||||
|
document = REXML::Document.new(xml)
|
||||||
|
REXML::XPath.match(document, xpath)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_boolean_true
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<child/>
|
||||||
|
<child/>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([true],
|
||||||
|
match(xml, "/root/child=true()"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_boolean_false
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([false],
|
||||||
|
match(xml, "/root/child=true()"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_number_true
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<child>100</child>
|
||||||
|
<child>200</child>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([true],
|
||||||
|
match(xml, "/root/child=100"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_number_false
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<child>100</child>
|
||||||
|
<child>200</child>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([false],
|
||||||
|
match(xml, "/root/child=300"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_string_true
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<child>text</child>
|
||||||
|
<child>string</child>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([true],
|
||||||
|
match(xml, "/root/child='string'"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_string_false
|
||||||
|
xml = <<-XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<child>text</child>
|
||||||
|
<child>string</child>
|
||||||
|
</root>
|
||||||
|
XML
|
||||||
|
assert_equal([false],
|
||||||
|
match(xml, "/root/child='nonexistent'"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue