fix(schema): apply cos-st-derived-ok to NameAndTypeOK over a union base (#359)

A particle restriction whose restricting element has a union-typed base element
was checked by structural member subsetting: PureXML accepted a separately
declared union whenever its members were a subset of the base union's. That is
not the spec's derivation relation. The NameAndTypeOK type clause (XSD 1.0
section 3.9.6) requires the restricting element's type to be validly derived
from the base element's type per Type Derivation OK (Simple), cos-st-derived-ok.

The restricting type is now valid over a union base only if it is the base
type, derives from it through a declared restriction chain, or (clause 2.2.4)
validly derives from one of the union's member types. Scoped to the
element-restriction path, so no attribute or simple-type derivation check moved.

Catches particlesIk011 (a fresh union(integer) is not a restriction of
union(integer, string)): XSTS invalid-schemas-accepted 9 to 8, valid-schemas-
rejected held at 0 over the full corpus (the cardinal no-over-rejection check).
Disclosed: particlesIk025/Ik027, where the restricting type derives from a
union member, are accepted per cos-st-derived-ok 2.2.4, a contested reading
their XSTS invalid expectation does not share. An in-repo test that asserted
the old structural-subset behavior is corrected.
This commit is contained in:
Mihaela Mihaljevic 2026-06-27 23:45:40 +02:00 committed by GitHub
parent fef8520d68
commit 0af92bcf8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 6 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- XSTS invalid-schemas-accepted (#145): a particle restriction whose restricting element has a union-typed base element now applies Type Derivation OK (Simple) (`cos-st-derived-ok`) rather than structural member subsetting. The NameAndTypeOK type clause (§3.9.6) requires the restricting element's type to be validly derived from the base element's type; PureXML accepted a *separately declared* union whenever its member types were a subset of the base union's members, which is not the spec's derivation relation. The restricting type is now valid over a union base only if it is the base type, derives from it through a declared restriction chain, or (clause 2.2.4) validly derives from one of the union's member types. Catches particlesIk011 (a fresh `union(integer)` is not a restriction of `union(integer, string)`): invalid-schemas-accepted 9 to 8, with valid-schemas-rejected held at 0 (verified over the full XSTS; the change is scoped to the element-restriction path, so no attribute or simple-type derivation check moved). Disclosed divergence: particlesIk025/Ik027, where the restricting type derives from a union *member*, are accepted per `cos-st-derived-ok` clause 2.2.4, a contested reading their XSTS `invalid` expectation does not share.
## [0.4.8] - 2026-06-27
### Removed

View File

@ -41,8 +41,17 @@ extension PureXML.Schema.ParticleRestriction {
if let baseResolved = resolveType(baseTypeName, types), let derivedResolved = resolveType(restrictedTypeName, types) {
switch (derivedResolved, baseResolved) {
case let (.simple(derivedSimple), .simple(baseSimple)):
if case .union = baseSimple.variety {
return PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: baseSimple)
if case let .union(baseMembers) = baseSimple.variety {
// cos-st-derived-ok with a union base: the restricting type
// validly derives iff it is the base type, derives from it
// through a declared restriction chain (typeDerivesByRestriction
// returns true for an equal name), or (clause 2.2.4) validly
// derives from one of the union's member types. A separately
// declared union whose members merely subset the base union's is
// NOT a derivation (particlesIk011); structural member subsetting
// is not the spec's derivation relation.
if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { return true }
return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) }
}
case (.complex, .simple):
return false

View File

@ -303,14 +303,18 @@ struct XSDParticleRestrictionTests {
// a union member (xs:string) validly restricts the union-typed base element.
#expect(doc("xs:string", base: "u", types: userTypes) != nil)
// a union restricting another union (subset members): valid.
let unions = """
<xs:simpleType name="U1"><xs:union memberTypes="xs:integer xs:string"/></xs:simpleType>
<xs:simpleType name="U2"><xs:union memberTypes="xs:integer xs:string xs:boolean"/></xs:simpleType>
<xs:simpleType name="L1"><xs:list itemType="xs:string"/></xs:simpleType>
"""
#expect(doc("U1", base: "U2", types: unions) != nil)
// a union restricting another union but adding/having unmatching members: invalid.
// A separately-declared union whose members merely subset the base union's
// is NOT a valid derivation: cos-st-derived-ok requires the restricting
// type to be the base, derive from it through a declared restriction chain,
// or derive from one of its members. Structural member subsetting is not
// the spec's derivation relation (cf. XSTS particlesIk011).
#expect(doc("U1", base: "U2", types: unions) == nil)
// The reverse (U2 has a member U1 lacks) is invalid for the same reason.
#expect(doc("U2", base: "U1", types: unions) == nil)
// a list type restricting a union type: invalid.
#expect(doc("L1", base: "U1", types: unions) == nil)

View File

@ -44,7 +44,13 @@ struct XSTSSuiteTests {
/// a base `required` attribute may not become `optional`) now also runs over a
/// `simpleContent` restriction, not only `complexContent` (particlesZ030_d,
/// which libxml2 also rejects).
private let knownSchemaInvalidAccepted = 9
/// 9 -> 8, NameAndTypeOK over a union base now applies cos-st-derived-ok: a
/// separately-declared union whose members merely subset the base union's is
/// not a valid derivation (particlesIk011); a genuine restriction of the union
/// or of one of its members still passes. (Ik025/Ik027, where the restricting
/// type derives from a union member, are accepted per cos-st-derived-ok 2.2.4,
/// a disclosed divergence from their contested XSTS `invalid` expectation.)
private let knownSchemaInvalidAccepted = 8
private let knownInstanceValidRejected = 0
/// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared
/// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's