Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions lib/rexml/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1269,20 +1269,17 @@ 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.

prefix = namespaces.key(namespace) if namespace
prefix = nil if prefix == 'xmlns'

ret_val =
attributes.get_attribute( prefix ? "#{prefix}:#{name}" : name )
return attributes.get_attribute( name ) if namespace.nil?

ret_val = attributes.get_attribute_ns( namespace, name )
return ret_val unless ret_val.nil?
return nil if prefix.nil?

# now check that prefix'es namespace is not the same as the
# default namespace
return nil unless ( namespaces[ prefix ] == namespaces[ 'xmlns' ] )

attributes.get_attribute( name )
# 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
end

# :call-seq:
Expand Down
28 changes: 28 additions & 0 deletions test/test_element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,33 @@ def test_array_reference_symbol
doc = REXML::Document.new("<language name='Ruby'/>")
assert_equal("Ruby", doc.root[:name])
end

def test_attribute_duplicated_namespace_url
doc = REXML::Document.new("<root xmlns='url1' xmlns:ns1='url1' " +
"xmlns:ns2='url2' xmlns:ns3='url2' " +
"a='' ns1:a='' ns1:b='' ns2:c='' ns3:d=''/>")
root = doc.root
attributes = [
root.attribute("a", "url1"),
root.attribute("b", "url1"),
root.attribute("c", "url2"),
root.attribute("d", "url2"),
]
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 = [
"<root xmlns:ns1='url1' xmlns='url1' a='A' ns1:a='NS1A'/>",
"<root a='A' xmlns:ns1='url1' xmlns='url1' ns1:a='NS1A'/>",
"<root xmlns:ns1='url1' xmlns='url1' ns1:a='NS1A' a='A'/>",
"<root xmlns='url1' xmlns:ns1='url1' ns1:a='NS1A' a='A'/>",
]
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