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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants