diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb
index 8881c3fa..e2fc148e 100644
--- a/lib/rexml/functions.rb
+++ b/lib/rexml/functions.rb
@@ -57,6 +57,7 @@ def count( node_set )
# Since REXML is non-validating, this method is not implemented as it
# requires a DTD
def id( object )
+ []
end
def local_name(node_set=nil)
diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb
index 9c856a65..7a9752f8 100644
--- a/lib/rexml/xpath_parser.rb
+++ b/lib/rexml/xpath_parser.rb
@@ -244,8 +244,10 @@ def expr( path_stack, nodeset, context=nil )
end
when :variable
var_name = path_stack.shift
- return @variables[var_name]
+ value = coerce_variable(@variables[var_name])
+ return value if path_stack.empty?
+ nodeset = apply_remaining_predicates(path_stack, value)
when :eq, :neq, :lt, :lteq, :gt, :gteq
left = expr( path_stack.shift, nodeset.dup, context )
right = expr( path_stack.shift, nodeset.dup, context )
@@ -319,15 +321,9 @@ def expr( path_stack, nodeset, context=nil )
when :group
sub_expression = path_stack.shift
result = expr(sub_expression, nodeset, context)
- if result.is_a?(Array)
- # If result is a nodeset, apply following predicates
- path_stack.unshift(:node)
- nodeset = step(path_stack) do
- [:iterate_nodesets, [XPathParser.sort(result)]]
- end
- else
- return result
- end
+ return result if path_stack.empty?
+
+ nodeset = apply_remaining_predicates(path_stack, result)
else
raise "[BUG] Unexpected path: <#{op.inspect}>: <#{path_stack.inspect}>"
end
@@ -337,6 +333,29 @@ def expr( path_stack, nodeset, context=nil )
leave(:expr, path_stack, nodeset) if @debug
end
+ def apply_remaining_predicates(path_stack, value)
+ # If evaluated value is not a nodeset, treat it as an empty nodeset.
+ # TODO: Decide whether REXML should raise type error or keep this behavior.
+ value = [] unless value.is_a?(Array)
+ path_stack.unshift(:node)
+ step(path_stack) do
+ [:iterate_nodesets, [XPathParser.sort(value)]]
+ end
+ end
+
+ # Coerces a variable value to a type that can be used in XPath expressions.
+ # TODO: Decide whether REXML should warn, raise, or ignore when a variable value is invalid.
+ def coerce_variable(value)
+ case value
+ when Array
+ value.grep(REXML::Node).uniq
+ when Numeric, String, true, false
+ value
+ else
+ ""
+ end
+ end
+
# Determines if a predicate expression is dependent on the position of nodes.
# Returns false if the expression is guaranteed to be position-independent.
# Returns true if the expression might be position-dependent.
diff --git a/test/xpath/test_base.rb b/test/xpath/test_base.rb
index 911fda12..0b3a0167 100644
--- a/test/xpath/test_base.rb
+++ b/test/xpath/test_base.rb
@@ -1523,5 +1523,37 @@ def test_reverse_axis_function_argument_sort
assert_equal(["e"], XPath.match(doc, "//e[10 + preceding-sibling::* = 11]").map(&:name))
assert_equal(["e"], XPath.match(doc, "//e[preceding-sibling::* = '1']").map(&:name))
end
+
+ def test_unimplemented_id_should_not_contaminate_nil
+ doc = Document.new("")
+ assert_equal([], XPath.match(doc, 'id("foo")'))
+ assert_equal([], XPath.match(doc, 'id("foo")[1]'))
+ assert_equal([], XPath.match(doc, 'id("foo")/bar'))
+ end
+
+ def test_variables
+ doc = Document.new("")
+ a, b, c, d, e = XPath.match(doc, '//*')
+ assert_equal([''], XPath.match(doc, '$x', nil, {}))
+ assert_equal([''], XPath.match(doc, '$x', nil, { 'x' => nil }))
+ assert_equal([''], XPath.match(doc, '$x', nil, { 'x' => Object.new }))
+ assert_equal([3], XPath.match(doc, 'count($x)', nil, { 'x' => [b, c, d] }))
+ assert_equal([3], XPath.match(doc, 'count($x)', nil, { 'x' => [a, a, b, b, c, c] }))
+ assert_equal([b, c, d], XPath.match(doc, '$x', nil, { 'x' => [d, c, b] }))
+ assert_equal([a], XPath.match(doc, '//*[name()=$x]', nil, { 'x' => 'a' }))
+ assert_equal([c, e], XPath.match(doc, '$x/*', nil, { 'x' => [b, d] }))
+ end
+
+ def test_variables_invalid_predicates
+ doc = Document.new("")
+ # Predicates after variable may be invalid depending on variable type.
+ # It can raise an exception such as TypeError, or treat the predicate result as an empty node set,
+ # but it should not return the variable value itself.
+ valid_result = [:exception, []]
+ actual = (XPath.match(doc, '$x[1<2]', nil, { 'x' => 42 }) rescue :exception)
+ assert_includes(valid_result, actual)
+ actual = (XPath.match(doc, '($x)[1<2]', nil, { 'x' => 42 }) rescue :exception)
+ assert_includes(valid_result, actual)
+ end
end
end