Change namespace cache strategy#333
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR refactors REXML namespace resolution to incorporate ATTLIST-declared xmlns values (and adds caching in the XPath parser), along with a new regression test that validates namespace precedence.
Changes:
- Add support for applying ATTLIST-declared xmlns values into element/attribute namespace resolution.
- Introduce per-parser caching for element namespace computations in
XPathParser. - Add a new test for ATTLIST vs element-declared namespace precedence; remove the previous namespaces cache regression test.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| test/xpath/test_base.rb | Removes a regression test that previously exercised namespace cache invalidation behavior. |
| test/test_core.rb | Adds a test asserting ATTLIST-declared xmlns precedence vs inherited/locally-declared namespaces. |
| lib/rexml/xpath_parser.rb | Adds namespace caching and routes namespace comparisons through cached lookup helpers. |
| lib/rexml/element.rb | Refactors namespace calculation/lookup internals and integrates ATTLIST-declared namespaces. |
| lib/rexml/document.rb | Adds extraction of ATTLIST-declared xmlns mappings per element name. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
8d141ec to
8a7840a
Compare
| @@ -2424,19 +2447,12 @@ def prefixes | |||
| # d.root.attributes.namespaces # => {"xmlns"=>"foo", "x"=>"bar", "y"=>"twee"} | |||
| # | |||
| def namespaces | |||
There was a problem hiding this comment.
This method is no longer called except from the added test.
We need to keep this if it's a public API, but if not, we can remove it (perhaps with deprecation first)
| namespaces = namespaces.merge(attlist_namespaces) if attlist_namespaces | ||
| namespaces = namespaces.merge(own_namespaces) if own_namespaces.any? |
There was a problem hiding this comment.
There was a small bug in master: precedence of attlist attributes and own attirbutes was reversed. Test is added for this
8a7840a to
95891d1
Compare
2856f4a to
58fa153
Compare
| def _attlist_mappings | ||
| mapping = {} | ||
| grep(AttlistDecl).each do |child| | ||
| raw_attributes = mapping[child.element_name] ||= {} | ||
| child.each do |key, val| | ||
| # First declaration wins | ||
| raw_attributes[key] = val unless raw_attributes.key? key | ||
| end | ||
| end | ||
| return nil unless att_decl | ||
| att_decl[attribute] | ||
| mapping | ||
| end |
58fa153 to
5d35322
Compare
| else | ||
| match( path_stack, node ) | ||
| end | ||
| @document = node.document |
There was a problem hiding this comment.
- before (expect)
> doc = REXML::Document.new '<!DOCTYPE a [<!ATTLIST b xmlns:p CDATA "urn:x">]><a><b><p:c/></b></a>'
> parser = REXML::XPathParser.new
> parser.predicate("//p:c", doc) #=> [<p:c/>]- after (this PR)
> doc = REXML::Document.new '<!DOCTYPE a [<!ATTLIST b xmlns:p CDATA "urn:x">]><a><b><p:c/></b></a>'
> parser = REXML::XPathParser.new
> parser.predicate("//p:c", doc) # => []Although @document is set in parse, it is not set in predicate or get_first, so @attlist_mappings becomes {} in those cases, and the xmlns declaration derived from ATTLIST does not take effect.
There was a problem hiding this comment.
Fixed. But I can't confidently say that it works because predicate is not tested at all, and first (called from get_first) is an incomplete function.
# FIXME: This method is incomplete!
def first( path_stack, node )
...
endMaybe it's worth deleting them in a separate pull request.
There was a problem hiding this comment.
Fixed.
Thanks
Maybe it's worth deleting them in a separate pull request.
We might want to consider deleting it.
3e780d9 to
a83e31a
Compare
a83e31a to
20c7dbc
Compare
20c7dbc to
032ab99
Compare
|
|
||
|
|
||
| def match(path_stack, node) | ||
| @document = node.document |
There was a problem hiding this comment.
When reusing an XPathParser, old cache entries remain.
- before (expect)
> doc = REXML::Document.new("<a xmlns='u1'><b/></a>")
> parser = REXML::XPathParser.new
> parser.namespaces = {"p" => "u1"}
> parser.parse("//p:b", doc)
=> [<b/>]
> doc.root
=> <a xmlns='u1'> ... </>
> doc.root.delete_namespace
=> <a> ... </>
> doc.root
=> <a> ... </>
> parser.parse("//p:b", doc)
=> []- after (this PR)
> doc = REXML::Document.new("<a xmlns='u1'><b/></a>")
> parser = REXML::XPathParser.new
> parser.namespaces = {"p" => "u1"}
> parser.parse("//p:b", doc)
=> [<b/>]
> doc.root.delete_namespace
=> <a> ... </>
> doc.root
=> <a> ... </>
> parser.parse("//p:b", doc)
=> [<b/>]| @document = node.document | |
| @document = node.document | |
| @attlist_mappings = nil | |
| @element_namespaces_cache = {} |
| else | ||
| match( path_stack, node ) | ||
| end | ||
| @document = node.document |
There was a problem hiding this comment.
Fixed.
Thanks
Maybe it's worth deleting them in a separate pull request.
We might want to consider deleting it.
Namespace calculation for each node can't be cached to document because document lookup is slow for deeply nested nodes. Change namespace cache strategy, inject cached hash as an argument to retrieve namespace/namespaces from XPath match operation and also for bare namespace call.
Co-authored-by: NAITOH Jun <naitoh@gmail.com>
032ab99 to
b9dcebe
Compare
| def namespaces | ||
| namespaces_cache = document&.__send__(:namespaces_cache) | ||
| if namespaces_cache | ||
| namespaces_cache[self] ||= calculate_namespaces | ||
| else | ||
| calculate_namespaces | ||
| end | ||
| _calculate_namespaces | ||
| end |
| def get_first path, node | ||
| path_stack = @parser.parse( path ) | ||
| @document = node.document | ||
| first( path_stack, node ) | ||
| end |
| def match(path_stack, node) | ||
| @document = node.document | ||
| nodeset = [node] | ||
| result = expr(path_stack, nodeset) |
| def attribute_of element, attribute | ||
| attribute_declarations_of(element)[attribute] | ||
| _attlist_mappings[element]&.[](attribute) | ||
| end | ||
|
|
||
| def attribute_declarations_of(name) | ||
| raw_attributes = {} | ||
| decls = select do |child| | ||
| child.kind_of?(AttlistDecl) && child.element_name == name | ||
| end | ||
| decls.each do |child| | ||
| def _attlist_mappings # :nodoc: |
Builds on top of #335
Even if namespace lookup is cached, namespace lookup was slow for deeply nested XML nodes.
Namespaces are cached in
document, butdocumentlookup from a deep node costsO(n).We can't cache it in a document or root of the node, because we also need to cache document lookup result.
It's not good to cache to node itself, for maintainability reason, cache invalidation, etc.
In this PR, Hash for caching node's namespace/namespaces, and cached namespace attributes defined in attlist decl can be injected through method parameters.
Benchmark
Time consumption was: Scan: 0.0046s (1.8%), Sort: 0.17s (60%), Namespace lookup: 0.11s (38%). This PR removes the 38% cost.
Cache through parameters also speeds up bare
namespacecall on deeply nested element.