Skip to content

Fix Element#attribute when a namespace URI is bound to multiple prefixes - #357

Open
sjh9714 wants to merge 2 commits into
ruby:masterfrom
sjh9714:codex/20260815-346-attribute-duplicated-namespace-url
Open

Fix Element#attribute when a namespace URI is bound to multiple prefixes#357
sjh9714 wants to merge 2 commits into
ruby:masterfrom
sjh9714:codex/20260815-346-attribute-duplicated-namespace-url

Conversation

@sjh9714

@sjh9714 sjh9714 commented Aug 15, 2026

Copy link
Copy Markdown

Fixes #346.

Motivation

Element#attribute can miss a namespaced attribute when the same namespace URI is bound to multiple prefixes. Lookups with both unprefixed and prefixed attributes could also depend on declaration order.

Attributes#get_attribute_ns resolves the namespaced match consistently. This change makes Element#attribute use that result first.

Changes

  • Keep the direct get_attribute lookup when no namespace is supplied.
  • When a namespace is supplied, call get_attribute_ns(namespace, name) first.
  • If that finds nothing, preserve the unprefixed fallback when the requested URI is the default namespace or is not declared on the element.

A matching result from get_attribute_ns now wins when both prefixed and unprefixed attributes exist.

Tests

  • Add coverage for a namespace URI bound to multiple prefixes.
  • Add coverage for all four relevant namespace and attribute declaration orders.
ruby test/run.rb

799 tests, 0 failures.

Element#attribute used namespaces.key(namespace), which returns only the
first prefix bound to that URI, so an attribute using any other prefix for
the same URI was not found. Try every prefix bound to the URI instead,
keeping the unprefixed attribute preferred when the URI is the default
namespace.

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 pull request fixes REXML::Element#attribute lookups when the same namespace URI is bound to multiple prefixes, ensuring attributes using any of those prefixes can be found (regression noted in #346, with context from prior refactor in #335).

Changes:

  • Update Element#attribute to try all prefixes bound to the requested namespace URI (while preserving the existing “default namespace prefers unprefixed attribute” behavior).
  • Add a regression test covering duplicated namespace-URI bindings and expected attribute resolution.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/rexml/element.rb Adjusts Element#attribute to consider multiple prefixes mapped to the same namespace URI.
test/test_element.rb Adds a regression test reproducing the duplicated-URI/prefix lookup failure from #346.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/rexml/element.rb Outdated

return ret_val unless ret_val.nil?
return nil if prefix.nil?
# An unprefixed attribute is used for the default namespace.
Comment thread lib/rexml/element.rb
@@ -1269,20 +1269,22 @@ def [](name_or_index)
# document.root.attribute("x", "a") # => a:x='a:x'
#
def attribute( name, namespace=nil )

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.

For this XML:

root1 = REXML::Document.new("<root xmlns:ns1='url1' xmlns='url1' a='A' ns1:a='NS1A'/>").root
root2 = REXML::Document.new("<root a='A' xmlns:ns1='url1' xmlns='url1' ns1:a='NS1A'/>").root
root3 = REXML::Document.new("<root xmlns:ns1='url1' xmlns='url1' ns1:a='NS1A' a='A'/>").root
root4 = REXML::Document.new("<root xmlns='url1' xmlns:ns1='url1' ns1:a='NS1A' a='A'/>").root

root.attributes.get_attribute_ns('url1', 'a').expanded_name always returns "ns1:a".
But attribute('a', 'url1') is different.

[root1, root2, root3, root4].map do |root|
  root.attribute('a', 'url1').expanded_name
end
# Master: ["ns1:a", "ns1:a", "ns1:a", "a"]
#   (the result depends on the declaration order of xmlns / xmlns:ns1 — this is a bug)
# This PR: ["a", "a", "a", "a"]

The behavior is changed, and it is changed to the opposite way from get_attribute_ns. Note that the PR description says "No lookup that previously returned an attribute returns a different one", but root1–root3 are counterexamples. It's worth describing this change in the PR description.
Though the expected behavior should be decided by the maintainer (@naitoh), I think it may be nice to align with get_attribute_ns.

If we're going to mainly align with get_attribute_ns, how about first calling get_attribute_ns, and falling back to a custom logic if nothing is found? The compatibility-oriented loose behaviors (matching an unprefixed attribute for the default namespace or for an undeclared namespace URI) can live in the fallback, and it would make it much easier to see how this method differs from get_attribute_ns.

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.

@kou

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.

OK. Let's do it.

Per review, look up the attribute via get_attribute_ns first, so a
prefixed attribute wins regardless of xmlns declaration order, and keep
the compatibility-oriented unprefixed fallback (default namespace or an
undeclared namespace URI) in one place.
@sjh9714

sjh9714 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Restructured in ac3e8b8 as you suggested. attribute now calls attributes.get_attribute_ns first, and the compatibility-oriented behaviors live in the fallback, so an unprefixed attribute still matches when the requested URI is the default namespace or is not declared on the element.

Your four examples now all return ns1:a, matching get_attribute_ns regardless of declaration order, and I added a regression test for exactly those documents. The existing test's attribute("a", "url1") expectation changed from a to ns1:a accordingly.

On the description's compatibility claim, you're right, root1-root3 were counterexamples. With this revision the remaining difference from master is narrower. A lookup can change only when the requested URI is the default namespace and both an unprefixed and a prefixed attribute are present. Master returned one or the other depending on xmlns declaration order, and this branch now always returns the prefixed one, like get_attribute_ns. The loose unprefixed fallbacks behave as before when no prefixed attribute matches. I can flip that priority if @naitoh prefers unprefixed-first for the default namespace.

Ran ruby test/run.rb, 799 tests, 0 failures.

@kou kou left a comment

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.

+1

Comment thread lib/rexml/element.rb
@@ -1269,20 +1269,22 @@ def [](name_or_index)
# document.root.attribute("x", "a") # => a:x='a:x'
#
def attribute( name, namespace=nil )

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.

OK. Let's do it.

@naitoh

naitoh commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@sjh9714
We think ac3e8b8 is good.
Could you please update the PR description to match ac3e8b8?

@sjh9714

sjh9714 commented Aug 21, 2026

Copy link
Copy Markdown
Author

Updated the PR description to match ac3e8b8 and removed the stale behavior claims. It now describes the get_attribute_ns lookup, the compatibility fallback and the reported 799-test result. Thanks.

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.

Element#attribute fails to find attribute when namespace URL is duplicated

5 participants