fix(schema): reject a nilled element with a fixed value constraint

cvc-elt.3.2.2: an element nilled by xsi:nil="true" may not have a fixed
{value constraint} (a nilled element has no value, so it cannot take a fixed
one). A nillable element MAY declare a fixed value, so the schema stays valid;
the conflict is an instance error only when such an element is actually nilled.
The check rides the shared nilErrors chokepoint, so the tree and streaming
validators enforce it identically; it prefers the matched particle's constraint
and falls back to the by-local-name map for particle-less paths (the unique-
named root). A nillable element that supplies its fixed value, a nilled element
with no fixed constraint, and a nilled element with a default (not fixed) value
are all unaffected. Catches addB065. XSTS invalid-instances-accepted 22 to 21,
valid-instances-rejected held at 0.
This commit is contained in:
Mihaela Mihaljevic 2026-06-22 17:54:03 +02:00
parent 1c836d50f2
commit e806c33a8e
5 changed files with 60 additions and 5 deletions

View File

@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A nilled element (an `xsi:nil="true"` on a nillable declaration) whose declaration carries a fixed `{value constraint}` is now rejected during instance validation (cvc-elt.3.2.2). A nillable element MAY declare a fixed value (the schema is valid); the conflict arises only when an instance actually nils such an element, since a nilled element has no value and so cannot take its fixed one. The check rides the existing `nilErrors` chokepoint (shared by the tree and streaming validators), using the matched particle's constraint with the by-local-name element-constraint map as a fallback, so it is enforced identically on both paths; a nillable element that supplies its fixed value, and a nilled element with no fixed value constraint, are both unaffected. Catches addB065. XSTS invalid-instances-accepted 22 → 21, valid-instances-rejected held at 0.
- A top-level (global) element or attribute declaration carrying a `form` attribute is now rejected even when it lives in an INCLUDED or IMPORTED document. The `form` attribute is permitted only on a local declaration (XSD 1.0 schema-for-schemas: `topLevelAttribute`/`topLevelElement` have no `form`, since a global component's qualification is fixed); the check (`topLevelDeclarationFindings`, which also enforces a global needing a `name` and forbids the other local-only `use`/`ref`/`minOccurs`/`maxOccurs`) already existed but ran only over the document being compiled, so a violation in an `xs:include`/`xs:import`/`xs:redefine` target was missed and the composed schema wrongly accepted. The same global-component check now runs over the other schema-document roots in the composition (the main document is excluded as already covered, and `redefine` entries are skipped because each schema root walks its own `redefine` children). The constraint is absolute and document-position-independent, so no valid schema is newly rejected; libxml2 rejects both cases for the same reason. Catches attQ016 (import) and attQ018 (include). XSTS invalid-schemas-accepted 20 → 18, valid-schemas-rejected held at 0.
- An identity-constraint field whose DECLARED target is a complex type with non-simple content is now rejected even when the instance node has no element children. The earlier fix rejected a field whose selected node carries element children (structural); this adds the case where the field's declared type is an empty or element-only/mixed complex type (an attributes-only element resolves to an empty-content complex type, so its instance node is childless yet not simple-typed). The field's target type is resolved through the schema type model (the selector target's type, then the field's same-named child element); a simple or simpleContent target, a type reached only through a named reference, a complex field path, or an unresolvable target is left alone, so no valid field is rejected. Catches idG006. XSTS invalid-instances-accepted 23 → 22, valid-instances-rejected held at 0.
- A substitution-group `block` on a head element now bars substituting a member whose INLINE (anonymous) type reaches the head's type by a blocked method. The substitution-group filter already dropped a member when the head's `{disallowed substitutions}` (or the head type's `{prohibited substitutions}`) blocked the member's derivation method, but only for members with a NAMED type recorded in the derivation backbone; an element with an inline complex type had no backbone entry, so a member extending the head's type was wrongly kept even when the head blocked `extension`. Inline-typed elements now record their derivation under a synthetic key (an element key plus a `#` marker no QName can contain, so it cannot collide with a real type and is unreachable as any other derivation base), so the filter sees the inline member's method. A member reaching the head's type by an UNblocked method, a plain inline type with no derivation, and same-local-name elements in different namespaces are all unaffected. Catches disallowedSubst00105m (and a sibling). XSTS invalid-instances-accepted 25 → 23, valid-instances-rejected held at 0.

View File

@ -92,7 +92,7 @@ public extension PureXML.Schema {
/// The errors from an `xsi:nil="true"` element: rejecting it when the
/// 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]? {
func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFailure]? {
let name = element.name.localName
// 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
@ -104,6 +104,12 @@ public extension PureXML.Schema {
if Self.hasContent(element) {
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
}
// cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}.
// The matched particle's constraint is authoritative; the by-local-name map
// is a fallback for paths without a particle (e.g. the resolved-type entry).
if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil {
return [XSDFailure(reason: "nil element '\(name)' must not have a fixed value constraint", at: path)]
}
return []
}
@ -318,7 +324,7 @@ public extension PureXML.Schema {
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path))
}
if let nilErrors = nilErrors(child, at: path) {
if let nilErrors = nilErrors(child, fixedValue: particleConstraint?.fixedValue, at: path) {
errors += nilErrors
return
}

View File

@ -194,7 +194,7 @@ public extension PureXML.Schema.ComplexValidator {
at path: XSDPath,
into errors: inout [XSDFailure],
) {
if let nilErrors = nilErrors(element, at: path) {
if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) {
errors += nilErrors
return
}

View File

@ -0,0 +1,47 @@
import Testing
@testable import PureXML
/// cvc-elt.3.2.2: a nilled element (xsi:nil="true" on a nillable declaration) may
/// not have a fixed {value constraint}. A nillable element MAY carry a fixed value
/// at the schema level (the declaration is valid); the conflict is an instance
/// error only when that element is actually nilled. Mirrors XSTS addB065.
@Suite("nilled element with a fixed value constraint")
struct SchemaNilFixedTests {
private func schema() throws -> PureXML.Schema.Document {
try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="r">
<xs:complexType>
<xs:sequence>
<xs:element name="i" type="xs:string" minOccurs="0" maxOccurs="unbounded"
nillable="true" fixed="abc"/>
<xs:element name="j" type="xs:string" minOccurs="0" nillable="true"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
""")
}
private let xsi = #"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance""#
@Test("the schema (nillable element with a fixed value) compiles")
func test_schemaValid() throws {
_ = try schema() // does not throw
}
@Test("nilling a fixed-valued element is rejected")
func test_nilledFixedRejected() throws {
#expect(try !schema().validate(#"<r \#(xsi)><i xsi:nil="true"/></r>"#).isEmpty)
}
@Test("supplying the fixed value (not nilling) is accepted")
func test_fixedValueAccepted() throws {
#expect(try schema().validate(#"<r \#(xsi)><i>abc</i></r>"#).isEmpty)
}
@Test("nilling a nillable element with no fixed value stays valid")
func test_nilledNoFixedAccepted() throws {
#expect(try schema().validate(#"<r \#(xsi)><j xsi:nil="true"/></r>"#).isEmpty)
}
}

View File

@ -38,8 +38,9 @@ struct XSTSSuiteTests {
/// type; a non-nillable element carries no xsi:nil; block/blockDefault bar substitutions (incl. an inline-
/// typed member reached by a blocked method, disallowedSubst00105m); ur-type anyType wildcards are `lax`;
/// identity fields compare in decimal value space (default/fixed when absent) and must be simple-content,
/// element-children (idK012) or empty-complex (idG006); an attr wildcard is the INTERSECTION; year 0000 invalid.
private let knownInstanceInvalidAccepted = 22
/// element-children (idK012) or empty-complex (idG006); an attr wildcard is the INTERSECTION; year 0000 invalid;
/// a nilled element may not have a fixed value constraint, cvc-elt.3.2.2 (addB065).
private let knownInstanceInvalidAccepted = 21
/// Suspect instance tests excluded from the counts: the Ethiopic-digit `\d`
/// cases contradict `\d` = `\p{Nd}` at Unicode 3.2 and are tracked as disputed.
private static let specDivergentInstances: Set<String> = ["reS17.v", "reS38.v", "reZ004v.v"]