[bugfix] Preserve attribute namespace when copied via enclosed expression (+4 XQTS HEAD)#6356
[bugfix] Preserve attribute namespace when copied via enclosed expression (+4 XQTS HEAD)#6356joewiz wants to merge 3 commits into
Conversation
… assertions XQueryTest.attributeNamespace and noNamepaceDefinedForPrefix_1959010 used assertEquals on serialized XML strings, which broke under the namespace-node ordering shift introduced by PR eXist-db#6356's fix. Per XQuery 3.1 §2 ("the relative order of namespace nodes that share a parent is also implementation dependent"), the new ordering is conformant — the assertions over-asserted on implementation-dependent serialization detail. Converted both to XMLUnit's DiffBuilder.checkForIdentical() pattern (matching existing usage at L1327-1333) which compares structurally, not byte-for-byte. No production code changes. The +4 XQTS HEAD lift (Constr-inscope-1..4) from PR eXist-db#6356's earlier commit is preserved. Full-module gate: Tests run: 6736, Failures: 0, Errors: 0, Skipped: 97, BUILD SUCCESS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
[This response was co-authored with Claude Code. -Joe] Pushed a follow-up commit (84762a9) addressing the two
Both were No production code changes; the +4 XQTS HEAD lift is preserved. Full-module |
|
@joewiz could you rebase this PR |
| * already declared there. No-op for the {@code xml} prefix or when the | ||
| * parent node is not an element. | ||
| */ | ||
| private void emitNamespaceNode(final String prefix, final String uri) { |
There was a problem hiding this comment.
Address Codacy issue: The method 'emitNamespaceNode(String, String)' has an NPath complexity of 240, current threshold is 200
…sion
XQuery 3.1 §3.9.4 (In-scope Namespaces of a Constructed Element)
requires that, for each prefix used in the name of an attribute of a
constructed element, a namespace binding must exist on the new element
-- and if adopting the source prefix would conflict with an existing
in-scope binding, the prefix MUST be changed to an
implementation-dependent prefix that does not cause a conflict, with a
new namespace binding created for it.
When an attribute node arrives in the content sequence of a direct
element constructor via an enclosed expression (for example
`<new xmlns:foo="B">{$x//@foo:k}</new>` where the source attribute is in
URI A), EnclosedExpr disables the receiver's namespace checking for the
duration of the copy and DocumentImpl.copyStartNode invokes
receiver.attribute(qname, value). The attribute was added to the
MemTree with its original (foo, A) QName but no namespace node was
emitted on the parent element for the (foo, A) binding -- and since
xmlns:foo on the new element was already bound to B, the serializer
emitted the attribute as foo:k in URI B, silently changing its
namespace. For inline two-attribute cases the second attribute was
collapsed into the same prefix as the first.
Fix: in DocumentBuilderReceiver.attribute, resolve a namespaced
attribute's prefix against the current in-scope namespaces independent
of the checkNS flag. When the prefix is unbound, declare it; when it is
bound to a different URI, generate a fresh prefix (or reuse an existing
one mapped to the source URI). In all cases emit a namespace node onto
the parent element so the binding is visible to the serializer. The
emission is idempotent against the parent element's own prefix and any
xmlns:* nodes already declared on it.
Closes 4 XQTS HEAD failures in prod-DirElemContent.namespace:
Constr-inscope-1, Constr-inscope-2, Constr-inscope-3, Constr-inscope-4.
XQTS prod-DirElemContent.namespace: 35 -> 31 failures (-4, no
regressions across the 133-test set).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… assertions XQueryTest.attributeNamespace and noNamepaceDefinedForPrefix_1959010 used assertEquals on serialized XML strings, which broke under the namespace-node ordering shift introduced by PR eXist-db#6356's fix. Per XQuery 3.1 §2 ("the relative order of namespace nodes that share a parent is also implementation dependent"), the new ordering is conformant — the assertions over-asserted on implementation-dependent serialization detail. Converted both to XMLUnit's DiffBuilder.checkForIdentical() pattern (matching existing usage at L1327-1333) which compares structurally, not byte-for-byte. No production code changes. The +4 XQTS HEAD lift (Constr-inscope-1..4) from PR eXist-db#6356's earlier commit is preserved. Full-module gate: Tests run: 6736, Failures: 0, Errors: 0, Skipped: 97, BUILD SUCCESS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pty bodies + reassigning param) Per @reinhapa's 2026-06-01 review on PR eXist-db#6356: > Address Codacy issue: The method 'emitNamespaceNode(String, String)' > has an NPath complexity of 240, current threshold is 200 Decompose emitNamespaceNode by extracting three named helpers that read like the F&O "must be true to skip" conditions the method is actually checking: - isElementParent(doc, parent) -- parent is a real element node - isParentSelfDeclaration(doc, parent, prefix, uri) -- the binding is redundant because the parent element name already carries it - hasExistingPrefixDeclaration(doc, parent, prefix) -- scan namespace decls already attached to parent Top-level method becomes four short guard clauses + the emit call. While in the file, a local codacy-cli run surfaced additional pre- existing nits that any future review-round would also catch: - 7 empty SAX-callback method bodies (setDocumentLocator, startCDATA, endCDATA, startDTD, endDTD, startEntity, endEntity, skippedEntity) -- add a one-line "no-op" comment to each explaining why the in-memory builder ignores the event. - generatePrefix(XQueryContext, String prefix) reassigned its parameter inside a while loop. Rewrite as an early-return for the "already supplied" path + a clean candidate-loop using an immutable parameter. Functionally equivalent. Codacy clean on the file post-refactor. 210 tests pass (XPathQueryTest 150, memtree tests 60). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84762a9 to
792e622
Compare
|
[This response was co-authored with Claude Code. -Joe] Rebased + addressed in 1. NPath complexity on
Top-level method becomes four short guard clauses + the emit call. 2. While I had the file open, a local Codacy run surfaced eight more pre-existing nits that any next review-round would also have caught. Folded them in here to save a round:
Codacy clean on the file post-refactor. 210 tests pass (XPathQueryTest 150, memtree tests 60), no behaviour changes. On the pre-push gap: I should have caught the NPath-240 finding before pushing the original commit. The repeated failure mode was running Codacy on net-new files but not on edits to existing ones. Tightened the rule on my side — mandatory pre-push Codacy on every changed Java file — and you should see fewer of these review-cycle-burn nits coming back. |
Summary
When a namespaced attribute is copied into a direct element constructor via an enclosed expression and the source attribute's prefix conflicts with a binding already declared on the new constructor, eXist silently changed the attribute's namespace URI to match the constructor's binding (or, for two-attribute cases, collapsed both attributes into the same prefix). For example:
returned
<out xmlns:foo=\"http://example.com/B\" foo:k=\"v\"/>-- attributekended up in URIB, not its source URIA.Spec rule
XQuery 3.1 §3.9.4 (In-scope Namespaces of a Constructed Element):
What changed
exist-core/src/main/java/org/exist/dom/memtree/DocumentBuilderReceiver.javaDocumentImpl.copyStartNodedispatches a copied attribute node toreceiver.attribute(qname, value). Within an enclosed expressionEnclosedExprdisables the receiver'scheckNSflag for the duration of the copy, so the attribute was previously added to the MemTree with its original(prefix, URI)QName but no namespace node was emitted on the parent constructor element for that binding. The serializer then resolved the attribute's prefix against the constructor's already-declaredxmlns:*and wrote it in the wrong URI.attribute()now resolves the QName for any namespaced attribute regardless ofcheckNS:XXX,XXX1, ...) and declare it.xmlns:*nodes already declared on it.exist-core/src/test/java/org/exist/xquery/ElementConstructorAttrNamespaceTest.javaThree JUnit regression tests covering the inline reproducers for Constr-inscope-3, Constr-inscope-4, and the minimal single-attribute case.
XQTS HEAD before / after
prod-DirElemContent.namespace(133 tests):No regressions in the rest of the test set.
Test plan
ElementConstructorAttrNamespaceTest)ConstructedNodesTest,ConstructedNodesRecoveryTest,NamespaceUpdateTest,CompAttrConstructorErrorCodeTest,DocumentBuilderReceiverIntegrationTest,XPathQueryTest(150),XQuery3Tests(1011),XIncludeSerializerTest: 1200 tests pass, 0 failuresW3CXIncludeTestSuitebaseline parity (37/183 failures both before and after on develop)prod-DirElemContent.namespace: -4 failures, 0 regressions🤖 Generated with Claude Code