From aca5642d58bfbce67133426eb7ee0d326b853e52 Mon Sep 17 00:00:00 2001
From: sjh9714 <163989462+sjh9714@users.noreply.github.com>
Date: Sat, 15 Aug 2026 10:35:05 +0900
Subject: [PATCH 1/2] Fix Element#attribute when a namespace URI is bound to
multiple prefixes
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.
---
lib/rexml/element.rb | 24 +++++++++++++-----------
test/test_element.rb | 15 +++++++++++++++
2 files changed, 28 insertions(+), 11 deletions(-)
diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb
index a002db07..878c5377 100644
--- a/lib/rexml/element.rb
+++ b/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 )
- prefix = namespaces.key(namespace) if namespace
- prefix = nil if prefix == 'xmlns'
+ prefixes = namespace ? namespaces.select {|_, uri| uri == namespace}.keys : []
- ret_val =
- attributes.get_attribute( prefix ? "#{prefix}:#{name}" : name )
-
- return ret_val unless ret_val.nil?
- return nil if prefix.nil?
+ # An unprefixed attribute is used for the default namespace.
+ if prefixes.empty? or prefixes.include?( 'xmlns' )
+ ret_val = attributes.get_attribute( name )
+ return ret_val unless ret_val.nil?
+ end
- # now check that prefix'es namespace is not the same as the
- # default namespace
- return nil unless ( namespaces[ prefix ] == namespaces[ 'xmlns' ] )
+ # The same namespace URI may be bound to multiple prefixes.
+ prefixes.each do |prefix|
+ next if prefix == 'xmlns'
+ ret_val = attributes.get_attribute( "#{prefix}:#{name}" )
+ return ret_val unless ret_val.nil?
+ end
- attributes.get_attribute( name )
+ nil
end
# :call-seq:
diff --git a/test/test_element.rb b/test/test_element.rb
index 20216895..f0fb30f6 100644
--- a/test/test_element.rb
+++ b/test/test_element.rb
@@ -11,5 +11,20 @@ def test_array_reference_symbol
doc = REXML::Document.new("")
assert_equal("Ruby", doc.root[:name])
end
+
+ def test_attribute_duplicated_namespace_url
+ doc = REXML::Document.new("")
+ root = doc.root
+ attributes = [
+ root.attribute("a", "url1"),
+ root.attribute("b", "url1"),
+ root.attribute("c", "url2"),
+ root.attribute("d", "url2"),
+ ]
+ assert_equal(["a", "ns1:b", "ns2:c", "ns3:d"],
+ attributes.collect {|attribute| attribute&.expanded_name})
+ end
end
end
From ac3e8b8aca9e52ad3a2ac09f49ca4754b2736666 Mon Sep 17 00:00:00 2001
From: sjh9714 <163989462+sjh9714@users.noreply.github.com>
Date: Tue, 18 Aug 2026 21:05:21 +0900
Subject: [PATCH 2/2] Align Element#attribute with Attributes#get_attribute_ns
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.
---
lib/rexml/element.rb | 21 ++++++++-------------
test/test_element.rb | 15 ++++++++++++++-
2 files changed, 22 insertions(+), 14 deletions(-)
diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb
index 878c5377..394d7415 100644
--- a/lib/rexml/element.rb
+++ b/lib/rexml/element.rb
@@ -1269,22 +1269,17 @@ def [](name_or_index)
# document.root.attribute("x", "a") # => a:x='a:x'
#
def attribute( name, namespace=nil )
- prefixes = namespace ? namespaces.select {|_, uri| uri == namespace}.keys : []
+ return attributes.get_attribute( name ) if namespace.nil?
- # An unprefixed attribute is used for the default namespace.
- if prefixes.empty? or prefixes.include?( 'xmlns' )
- ret_val = attributes.get_attribute( name )
- return ret_val unless ret_val.nil?
- end
+ ret_val = attributes.get_attribute_ns( namespace, name )
+ return ret_val unless ret_val.nil?
- # The same namespace URI may be bound to multiple prefixes.
- prefixes.each do |prefix|
- next if prefix == 'xmlns'
- ret_val = attributes.get_attribute( "#{prefix}:#{name}" )
- return ret_val unless ret_val.nil?
+ # Kept for compatibility: an unprefixed attribute also matches when the
+ # requested URI is the default namespace or is not declared on this
+ # element, even though such an attribute is not in that namespace.
+ if namespace == namespaces[ 'xmlns' ] or !namespaces.has_value?( namespace )
+ attributes.get_attribute( name )
end
-
- nil
end
# :call-seq:
diff --git a/test/test_element.rb b/test/test_element.rb
index f0fb30f6..02dae3be 100644
--- a/test/test_element.rb
+++ b/test/test_element.rb
@@ -23,8 +23,21 @@ def test_attribute_duplicated_namespace_url
root.attribute("c", "url2"),
root.attribute("d", "url2"),
]
- assert_equal(["a", "ns1:b", "ns2:c", "ns3:d"],
+ assert_equal(["ns1:a", "ns1:b", "ns2:c", "ns3:d"],
attributes.collect {|attribute| attribute&.expanded_name})
end
+
+ def test_attribute_prefixed_match_independent_of_declaration_order
+ sources = [
+ "",
+ "",
+ "",
+ "",
+ ]
+ expanded_names = sources.collect do |source|
+ REXML::Document.new(source).root.attribute("a", "url1").expanded_name
+ end
+ assert_equal(["ns1:a"] * 4, expanded_names)
+ end
end
end