Fix xpath functions related to names#343
Conversation
Fix and simplify name(nodesets), local-name(nodesets) and namespace-uri(nodesets) node select logic. All functions that uses a single node should use the first document-ordered node, but these functions were wrongly skipping un-named nodes.
There was a problem hiding this comment.
Pull request overview
This PR fixes XPath name-related functions (name(), local-name(), namespace-uri()) so they correctly use the first document-ordered node (even if it’s not a named node), rather than skipping unnamed nodes and selecting a later named node.
Changes:
- Refactors
local_name,name, andnamespace_urito share a simplified “pick first document-ordered node” helper. - Updates and expands
test_local_nameto avoid whitespace-text-node sensitivity and to cover unnamed-node behavior. - Adds new test cases for attribute and non-named node inputs.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
lib/rexml/functions.rb |
Simplifies node selection logic for name-related XPath functions via a new helper method. |
test/functions/test_local_name.rb |
Adjusts existing node-set construction and adds new cases to validate the corrected behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result | ||
| elsif node_set.respond_to? :namespace | ||
| yield node_set | ||
| def target_named_node(node_set = nil) |
| when nil | ||
| node = @context[:node] | ||
| when Array | ||
| node = XPathParser.sort(node_set).first | ||
| end |
There was a problem hiding this comment.
There is regression.
- before(master)
> doc = REXML::Document.new("<root xmlns:x='http://example.com/x/'><x:child/></root>")
> REXML::Functions.local_name(doc.root.elements[1]) #=> "child"
> REXML::Functions.local_name("not a node") #=> ""- after(this PR)
> doc = REXML::Document.new("<root xmlns:x='http://example.com/x/'><x:child/></root>")
> REXML::Functions.local_name(doc.root.elements[1]) #=> ""
> REXML::Functions.local_name("not a node") #=> ""| when nil | |
| node = @context[:node] | |
| when Array | |
| node = XPathParser.sort(node_set).first | |
| end | |
| when nil | |
| node = @context[:node] | |
| when Array | |
| node = XPathParser.sort(node_set).first | |
| else | |
| node = node_set | |
| end |
There was a problem hiding this comment.
Is it really a regression? I think it's a dead branch which should be cleaned up.
XPath values are: boolean, string, number, and nodeset. there is no non-nodeset-single-node.
There was a problem hiding this comment.
If REXML::Functions.local_name(node) #=> node.local_name is considered a public API, it is better to not change the behavior. What do you think?
There was a problem hiding this comment.
REXML::Functions.local_name(node) #=> node.local_name is not part of the public API.
This change is not a regression.
I'm sorry.
https://docs.ruby-lang.org/ja/latest/class/REXML=3a=3aFunctions.html
内部用なのでユーザは使わないでください。
(I'm sorry for writing in Japanese.)
There was a problem hiding this comment.
I rewrote the code so that it doesn't use REXML::Functions.local_name(node).
- before(rexml 3.4.4)
> doc = REXML::Document.new("<root xmlns:x='http://example.com/x/'><x:child/></root>")
> child = doc.root.elements[1] #=> <x:child/>
> REXML::XPath.match(doc, "local-name($x)", nil, { "x" => child }) #=> ["child"]
> REXML::XPath.match(doc, "local-name('not a node')") #=> [""]- after(this PR)
> doc = REXML::Document.new("<root xmlns:x='http://example.com/x/'><x:child/></root>")
> child = doc.root.elements[1] #=> <x:child/>
> REXML::XPath.match(doc, "local-name($x)", nil, { "x" => child }) #=> [""]
> REXML::XPath.match(doc, "local-name('not a node')") #=> [""]XPath values are: boolean, string, number, and nodeset. there is no non-nodeset-single-node.
However, I agree that “single nodes do not need to be considered.”
There was a problem hiding this comment.
The variable in the code below is invalid for now.
REXML::XPath.match(doc, "local-name($x)", nil, { "x" => child })Example:
REXML::XPath.match(doc, "($x)/root", nil, { "x" => [doc] })
# => [<root xmlns:x='http://example.com/x/'> ... </>]
REXML::XPath.match(doc, "($x)/root", nil, { "x" => doc })
# => [<UNDEFINED> ... </>]It will be valid in #342 (comment)
| node_set = document.root.elements.to_a | ||
| assert_equal("child", REXML::Functions.local_name(node_set)) |
| def test_attribute | ||
| document = REXML::Document.new("<root xmlns:x='http://example.com/x/' x:attr='value' />") | ||
| node_set = [document.root.attributes.to_a.last] | ||
| assert_equal("attr", REXML::Functions.local_name(node_set)) | ||
| end |
| assert_equal("", REXML::Functions.local_name([children[0]])) | ||
| assert_equal("", REXML::Functions.local_name([children[1]])) | ||
| assert_equal("", REXML::Functions.local_name(children)) |
Fix and simplify name(nodesets), local-name(nodesets) and namespace-uri(nodesets) node select logic. All functions that uses a single node should use the first document-ordered node, but these functions were wrongly skipping un-named nodes.