Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/rexml/parsers/xpathparser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class XPathParser # :nodoc:
include XMLTokens
LITERAL = /^'([^']*)'|^"([^"]*)"/u

def initialize(strict: false)
@strict = strict
end

def namespaces=( namespaces )
Functions::namespace_context = namespaces
@namespaces = namespaces
Expand Down Expand Up @@ -653,6 +657,7 @@ def PrimaryExpr path, parsed
#arry << @variables[ varname ]
when /^(\w[-\w]*)(?:\()/
fname = $1
return path if @strict && fname.include?("_")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism that this return path rejects the xpath, is complicated. (See LocationPath → RelativeLocationPath → NodeTest called when 'local_name()' is passed)

I think underscored function names are invalid not because it has underscore, but because it's not registered as a function in XPath 1.0 spec.
Both examples in the code below is in the same situation: using unknown function, so the result (return value or raised error) should be the same.

REXML::XPath.match(REXML::Document.new('<root/>'), 'local_name()')
REXML::XPath.match(REXML::Document.new('<root/>'), 'localname()')

From a viewpoint of module's responsibility, I personally think this check shouldn't be done in REXML::Parsers:XPathParser, but in one of:

  • REXML::XPathParser: layer that converts '-' to '_', function name to a method name
  • REXML::FunctionsClass: layer that knows all available functions, checks if a method name is registered as a function

tmp = $'
return path if fname =~ NT
path = tmp
Expand Down
2 changes: 1 addition & 1 deletion lib/rexml/xpath_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class XPathParser

def initialize(strict: false)
@debug = DEBUG
@parser = REXML::Parsers::XPathParser.new
@parser = REXML::Parsers::XPathParser.new(strict: strict)
@namespaces = nil
@variables = {}
@functions = FunctionsClass.new
Expand Down
9 changes: 8 additions & 1 deletion test/functions/test_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,18 @@ def test_string_nil_without_context
{"n" => nil}))
end

def test_unregistered_method
def test_unregistered_method_with_underscore
doc = Document.new("<root/>")
assert_nil(XPath::first(doc.root, "to_s()"))
end

def test_unregistered_method_with_underscore_in_strict_mode
doc = Document.new("<root/>")
assert_raise(REXML::ParseException) do
XPath::first(doc.root, "to_s()", nil, {}, strict: true)
end
end

def test_nonexistent_function
doc = Document.new("<root><nonexistent/></root>")
# TODO: Maybe, this is not XPath spec behavior.
Expand Down
13 changes: 13 additions & 0 deletions test/parser/test_xpath.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: false

require "test/unit"
require "rexml/parseexception"
require "rexml/parsers/xpathparser"

module REXMLTests
Expand Down Expand Up @@ -66,6 +67,18 @@ def test_function
abbreviate("string-length(a/b[last()])"))
end

def test_function_with_underscore
assert_equal("local_name(*)",
abbreviate("local_name(*)"))
end

def test_function_with_underscore_in_strict_mode
parser = REXML::Parsers::XPathParser.new(strict: true)
assert_raise(REXML::ParseException) do
parser.abbreviate("local_name(*)")
end
end

def test_descendant_or_self_only
assert_equal("//",
abbreviate("/descendant-or-self::node()/"))
Expand Down