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