Skip to content

Fix xpath functions related to names - #343

Merged
naitoh merged 3 commits into
ruby:masterfrom
tompng:name_function_fix
Jul 31, 2026
Merged

Fix xpath functions related to names#343
naitoh merged 3 commits into
ruby:masterfrom
tompng:name_function_fix

Conversation

@tompng

@tompng tompng commented Jun 28, 2026

Copy link
Copy Markdown
Member

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.

xml = '<root>text<!-- comment --><node/></root>'
xpath = 'name(root/node())'

REXML::XPath.match(REXML::Document.new(xml), xpath)
#=> ["node"] (bug) → [""]
Nokogiri::XML.parse(xml).xpath(xpath)
#=> "" (expected)
Copilot AI review requested due to automatic review settings June 28, 2026 08:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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, and namespace_uri to share a simplified “pick first document-ordered node” helper.
  • Updates and expands test_local_name to 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.

Comment thread lib/rexml/functions.rb
Comment thread lib/rexml/functions.rb
Comment on lines +78 to 82
when nil
node = @context[:node]
when Array
node = XPathParser.sort(node_set).first
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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")  #=> ""
Suggested change
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.”

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be valid in #342 (comment)

After rebasing, I confirmed that it works as expected.
Thank you.

Comment thread test/functions/test_local_name.rb
Comment thread test/functions/test_local_name.rb
Comment thread test/functions/test_local_name.rb
@naitoh

naitoh commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@tompng
Could you please rebase this PR?

tompng added 2 commits July 29, 2026 17:11
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.
Copilot AI review requested due to automatic review settings July 29, 2026 08:12
@tompng
tompng force-pushed the name_function_fix branch from f280f78 to f5c707d Compare July 29, 2026 08:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

lib/rexml/functions.rb:75

  • The PR changes name()/namespace-uri() selection semantics via target_named_node, but the added regression tests only cover local-name(). There’s currently no test that exercises name(node-set) and namespace-uri(node-set) when the first document-ordered node is non-named (e.g., node() returning text/comment before an element), which is the class of bug described in the PR.

Add tests similar to test_non_named that assert REXML::XPath.match(doc, 'name(root/node())') == [''] and namespace-uri(root/node()) == [''] (and an attribute case for namespace-uri()).

    def name( node_set=nil )
      target_named_node(node_set)&.expanded_name || ""
    end
@tompng

tompng commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Rebase done.
Applied the copilot suggession

- node_set = [document.root.attributes.to_a.last]
+ node_set = [document.root.attributes.get_attribute("x:attr")]

Other copilot suggestion: ignored.
test_local_name.rb should only contain local_name test unless the file is renamed.
It is actually testing common part of name/local-name/namespace: target node selection, so the basic part for other functions are already tested here, and slightly tested in other existing test (e.g. XPath tests).

Comment thread lib/rexml/functions.rb Outdated
Co-authored-by: NAITOH Jun <naitoh@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 07:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

test/functions/test_local_name.rb:41

  • This test only asserts local_name, but this PR also changes name() and namespace_uri() to use the first document-ordered node (including non-named nodes). Adding assertions here for attribute nodes would help prevent regressions in the other two functions.
      document = REXML::Document.new("<root xmlns:x='http://example.com/x/' x:attr='value' />")
      node_set = [document.root.attributes.get_attribute("x:attr")]
      assert_equal("attr", REXML::Functions.local_name(node_set))

test/functions/test_local_name.rb:49

  • test_non_named validates local_name for text/comment nodes, but the same selection logic is now shared by name() and namespace_uri(). Adding assertions for those functions here would better cover the intended bug fix across all three functions.
      document = REXML::Document.new("<root>text<!-- comment --><a/></root>")
      children = document.root.children
      assert_equal("", REXML::Functions.local_name([children[0]]))
      assert_equal("", REXML::Functions.local_name([children[1]]))
      assert_equal("", REXML::Functions.local_name(children))

@naitoh naitoh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

@naitoh
naitoh merged commit fa0427c into ruby:master Jul 31, 2026
71 checks passed
@tompng
tompng deleted the name_function_fix branch July 31, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants