PureXML/Tests/SchemaAttributeUniquenessTests.swift
Mihaela Mihaljevic 067f04184f fix(schema): enforce attribute-use uniqueness and the single-ID rule
A complex type's or attribute group's attribute-use set (flattened through
attributeGroup references) may not contain two distinct declarations with the
same name and namespace (ct-props-correct.4 / ag-props-correct.2), nor more than
one attribute whose type is or derives from xs:ID (ct-props-correct.5).

Declarations are deduplicated by the identity of their declaring node, so the
same declaration reached by two attribute-group paths (a diamond) is one use,
not a clash. Names carry their resolved namespace (form / attributeFormDefault
for locals, the prefix binding for refs), so a local attribute and a reference
to a same-local-name attribute in an imported namespace do not collide. The
check runs after named simple types are resolved, so a named user type derived
from xs:ID is recognized, not just the built-in and inline forms.

XSTS: invalid schemas accepted 505 to 495 (attQ008/009/012/013, attgD006/007/
009/017, ctM003/004 now rejected), no other bucket raised. An adversarial critic
caught a false positive on same-local-name-different-namespace (fixed by
resolving the ref namespace) and a missed named-user-ID type (fixed by ordering
the check after type resolution). Disclosed under-rejections: the set inherited
through extension is not composed in; a duplicate needing an unprefixed
reference with no default namespace is not detected.
2026-06-15 10:29:40 +02:00

57 lines
3.5 KiB
Swift

@testable import PureXML
import Testing
@Suite("XSD attribute-use uniqueness and single ID")
struct SchemaAttributeUniquenessTests {
private func compiles(_ source: String) -> Bool {
(try? PureXML.Schema.Document(source)) != nil
}
private let xsd = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
@Test("Two attribute uses with the same name in one type or group are rejected")
func test_duplicateAttributeRejected() {
// Two local attributes of the same name directly in a complex type.
#expect(!compiles("<xs:schema \(xsd)><xs:complexType name=\"T\">"
+ "<xs:attribute name=\"a\" type=\"xs:string\"/><xs:attribute name=\"a\" type=\"xs:int\"/>"
+ "</xs:complexType></xs:schema>"))
// A local attribute colliding with one pulled in through an attribute group.
#expect(!compiles("<xs:schema \(xsd)>"
+ "<xs:attributeGroup name=\"g\"><xs:attribute name=\"a\" type=\"xs:string\"/></xs:attributeGroup>"
+ "<xs:complexType name=\"T\"><xs:attribute name=\"a\" type=\"xs:int\"/><xs:attributeGroup ref=\"g\"/></xs:complexType></xs:schema>"))
// Two same-named attributes within one attribute group (ag-props-correct.2).
#expect(!compiles("<xs:schema \(xsd)><xs:attributeGroup name=\"g\">"
+ "<xs:attribute name=\"a\"/><xs:attribute name=\"a\"/></xs:attributeGroup></xs:schema>"))
}
@Test("More than one ID-typed attribute is rejected (ct-props-correct.5)")
func test_twoIDAttributesRejected() {
#expect(!compiles("<xs:schema \(xsd)><xs:complexType name=\"T\">"
+ "<xs:attribute name=\"a\" type=\"xs:ID\"/><xs:attribute name=\"b\" type=\"xs:ID\"/>"
+ "</xs:complexType></xs:schema>"))
// A single ID attribute is fine.
#expect(compiles("<xs:schema \(xsd)><xs:complexType name=\"T\">"
+ "<xs:attribute name=\"a\" type=\"xs:ID\"/><xs:attribute name=\"b\" type=\"xs:string\"/>"
+ "</xs:complexType></xs:schema>"))
}
@Test("Same local name in different namespaces, and a diamond, are not clashes")
func test_distinctNamespacesAndDiamondAccepted() {
// attQ010/attQ019: a local attribute and a reference to a same-local-name
// attribute in an imported namespace are different attributes. (Must compile.)
#expect(compiles("<xs:schema \(xsd) targetNamespace=\"urn:t\" xmlns:t=\"urn:t\""
+ " xmlns:imp=\"urn:imp\" attributeFormDefault=\"qualified\">"
+ "<xs:import namespace=\"urn:imp\"/>"
+ "<xs:attributeGroup name=\"g\"><xs:attribute name=\"a\"/><xs:attribute ref=\"imp:a\"/></xs:attributeGroup>"
+ "<xs:complexType name=\"T\"><xs:attributeGroup ref=\"t:g\"/></xs:complexType></xs:schema>"))
// A diamond of attribute-group references reaching the same attribute once is
// not a duplicate: g3 has attribute a; g1 and g2 both include g3; T includes
// both. (Must compile.)
#expect(compiles("<xs:schema \(xsd)>"
+ "<xs:attributeGroup name=\"g3\"><xs:attribute name=\"a\"/></xs:attributeGroup>"
+ "<xs:attributeGroup name=\"g1\"><xs:attributeGroup ref=\"g3\"/></xs:attributeGroup>"
+ "<xs:attributeGroup name=\"g2\"><xs:attributeGroup ref=\"g3\"/></xs:attributeGroup>"
+ "<xs:complexType name=\"T\"><xs:attributeGroup ref=\"g1\"/><xs:attributeGroup ref=\"g2\"/></xs:complexType></xs:schema>"))
}
}