fix(schema): a non-nillable element must carry no xsi:nil at all (cvc-elt.3.1)
cvc-elt.3.1: when an element's declaration is not nillable, the xsi:nil attribute is forbidden entirely, whatever its value. PureXML only rejected xsi:nil="true" on a non-nillable element, so xsi:nil="false" slipped through. ComplexValidator.nilErrors now flags the presence of any xsi:nil on a non-nillable element via the new nilAttributeValue helper, while a nillable element still accepts xsi:nil="false" (explicitly not nilled, valid empty-or-content per its type). XSTS invalid-instances-accepted 28 -> 26 (both xsi:nil-on-non-nillable cases), no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at 0 across all 14383 groups). 1452 unit tests pass.
This commit is contained in:
parent
45f2e7dbf5
commit
2a9a5afaa9
@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A non-nillable element may now carry no `xsi:nil` attribute at all, whatever its
|
||||
value (cvc-elt.3.1). Previously only `xsi:nil="true"` on a non-nillable element
|
||||
was rejected; `xsi:nil="false"` slipped through. `ComplexValidator.nilErrors` now
|
||||
flags the presence of any `xsi:nil` on a non-nillable element, while a nillable
|
||||
element still accepts `xsi:nil="false"` (explicitly not nilled). XSTS
|
||||
invalid-instances-accepted 28 -> 26, no other bucket moved (valid-rejected held
|
||||
at 0 across all 14383 groups).
|
||||
|
||||
- An element with neither a `type` attribute nor an inline type, but a
|
||||
`substitutionGroup`, now takes the type definition of its head (XSD 1.0 3.3.2)
|
||||
instead of defaulting to `anyType`. `XSDParser.elementType` resolves the head
|
||||
|
||||
@ -86,11 +86,14 @@ public extension PureXML.Schema {
|
||||
/// element is not nillable, or when it carries content. Returns nil when
|
||||
/// the element is not nilled.
|
||||
func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
|
||||
guard Self.isNil(element) else { return nil }
|
||||
let name = element.name.localName
|
||||
if !nillableElements.contains(name) {
|
||||
// cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at
|
||||
// all, whatever its value (so `xsi:nil="false"` is equally forbidden, not
|
||||
// only `xsi:nil="true"`).
|
||||
if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) {
|
||||
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
|
||||
}
|
||||
guard Self.isNil(element) else { return nil }
|
||||
if Self.hasContent(element) {
|
||||
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
|
||||
}
|
||||
|
||||
@ -185,11 +185,17 @@ extension PureXML.Schema.ComplexValidator {
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// Whether the element carries `xsi:nil="true"`.
|
||||
static func isNil(_ element: PureXML.Model.Element) -> Bool {
|
||||
element.attributes.contains { attribute in
|
||||
nilAttributeValue(element) == "true"
|
||||
}
|
||||
|
||||
/// The value of the `xsi:nil` attribute if present (any value), or nil if the
|
||||
/// element carries no `xsi:nil`. Used for the cvc-elt.3.1 rule that a
|
||||
/// non-nillable element must carry no `xsi:nil` attribute at all.
|
||||
static func nilAttributeValue(_ element: PureXML.Model.Element) -> String? {
|
||||
element.attributes.first { attribute in
|
||||
attribute.name.localName == "nil"
|
||||
&& (attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi")
|
||||
&& attribute.value == "true"
|
||||
}
|
||||
}?.value
|
||||
}
|
||||
|
||||
/// Whether the element has any child element or non-whitespace text.
|
||||
|
||||
@ -90,6 +90,18 @@ struct XSDCategoryTests {
|
||||
#expect(validate(element("a", [xsi]), type, validator: nillable).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Nillable: xsi:nil='false' is also forbidden on a non-nillable element (cvc-elt.3.1)")
|
||||
func test_notNillableNilFalse() {
|
||||
let nilFalse = ("xsi:nil", "false")
|
||||
let type = Schema.ComplexType(content: .empty)
|
||||
// cvc-elt.3.1: a non-nillable element must carry no xsi:nil attribute at all,
|
||||
// so even xsi:nil="false" is rejected (not only "true").
|
||||
#expect(validate(element("a", [nilFalse]), type).contains { $0.reason.contains("not nillable") })
|
||||
// A nillable element may carry xsi:nil="false" (explicitly not nilled): accepted.
|
||||
let nillable = Schema.ComplexValidator(nillableElements: ["a"])
|
||||
#expect(validate(element("a", [nilFalse]), type, validator: nillable).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Fixed: an element fixed value must match")
|
||||
func test_fixedElement() {
|
||||
let validator = Schema.ComplexValidator(elementConstraints: ["a": .fixed("yes")])
|
||||
|
||||
@ -150,13 +150,13 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 43
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 28), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in
|
||||
/// for an anySimpleType element; an untyped substitutionGroup member inherits its head's type;
|
||||
/// block/blockDefault bar substitutions on the namespace-keyed backbone; ur-type anyType wildcards
|
||||
/// are `lax`; identity fields compare in decimal value space, taking a declared default/fixed when
|
||||
/// for anySimpleType; an untyped substitutionGroup member inherits its head's type; a non-nillable
|
||||
/// element carries no xsi:nil (cvc-elt.3.1); block/blockDefault bar substitutions; ur-type anyType
|
||||
/// wildcards are `lax`; identity fields compare in decimal value space, taking a default/fixed when
|
||||
/// absent/empty; an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid.
|
||||
private let knownInstanceInvalidAccepted = 28
|
||||
private let knownInstanceInvalidAccepted = 26
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
@ -91,7 +91,7 @@ classification is wrong.
|
||||
|---|---|---|
|
||||
| Target namespace / form matching | `targetns00101m/targetNS00101m1_n` | `ComplexValidator*`, reference resolution |
|
||||
| Disallowed substitution | `disallowedsubst00105m/disallowedSubst00105m1_n`, `disallowedsubst00106m2/Negative` | `XSDSubstitutionMembers`, `ComplexValidatorXSIType` |
|
||||
| Nillability | `nillable00201m/nillable00201m2_n` | `XSDParserHelpers`, instance validator |
|
||||
| Nillability | ~~`nillable00201m/nillable00201m2_n`~~ closed: a non-nillable element carries no `xsi:nil` at all, even `"false"` (cvc-elt.3.1) | `ComplexValidator.nilErrors` |
|
||||
| Type derivation | ~~`typedef00204m/typeDef00204m1_n`~~ closed: an untyped `substitutionGroup` member inherits its head's type (XSD 1.0 3.3.2) | `XSDParser.elementType` |
|
||||
| Additional-suite instance triage | `addB065/addB065.i` | to classify from fixture |
|
||||
| Datatype value-space edges | `anyURI_a004_1339/anyURI_a004_1339.i` (open); ~~`dateTime011_2008/dateTime011_2008.i`~~ closed: XSD 1.0 has no year zero, `0000` now rejected | `SimpleType`, `DateTimeParser` |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user