fix(schema): restriction may not narrow an element to an extension-derived type

XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2: when a complexContent
restriction narrows an element, the element's type must be validly derived
from the base element's type EXCLUDING extension (and list/union), i.e. by
restriction only. The element-type check followed the derivation chain
regardless of method, so a type derived from the base by extension was wrongly
accepted as a valid restriction (corpus particlesIj008).

Add typeDerivesByRestriction (as typeDerivesOrEqual, but the user-declared
chain is followed across restriction steps only; an extension step breaks the
walk) and use it on the non-union fall-through of elementTypeRestrictionOK.
The shared typeDerivesOrEqual is untouched, so substitution-group validity
(where extension is legal) is unaffected. Built-in lattice and union-base
derivation are unchanged, so valid restrictions, including the union-member
restriction addB150, still compile.

XSTS invalid-schemas-accepted 37 -> 36; valid-schemas-rejected held at 0
across all 14383 groups. Union-base (Ik011/Ik025/Ik027) and substitution-group
(V020) cases need the by-name union-member and ref-resolution clauses and stay
deferred.
This commit is contained in:
Mihaela Mihaljevic 2026-06-20 18:12:58 +02:00
parent f6159191db
commit ee676cb470
4 changed files with 86 additions and 3 deletions

View File

@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A complexContent restriction that narrows an element to a type derived from the
base element's type by **extension** is now rejected (XSD 1.0 cos-particle-restrict
NameAndTypeOK.2.2, #158): the element-type-derivation check follows the derivation
chain across `restriction` steps only, so an extension-derived type is not a valid
restriction (corpus particlesIj008). Built-in lattice and union-base derivation are
unchanged. XSTS invalid-schemas-accepted 37 -> 36, valid-schemas-rejected held at 0.
- A `fixed` value on an element whose type is a complex type with `simpleContent` is
now compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw string
(#202), so `05` satisfies an `xs:int` `fixed="5"`. The complex-element fixed check on

View File

@ -54,7 +54,12 @@ extension PureXML.Schema.ParticleRestriction {
}
if baseIsUnion(baseTypeName, types) { return true }
return typeDerivesOrEqual(restrictedTypeName, baseTypeName, derivation, types)
// NameAndTypeOK.2.2: a restricting element's type must be validly derived from
// the base element's type EXCLUDING extension (and list/union), i.e. by
// restriction only. A type that reaches the base through an extension step is
// not a valid restriction. (Union-base cases are still handled above; the
// by-name restriction-derivation of a union member is a separate clause.)
return typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types)
}
/// Whether the named type resolves (through the type table) to a `union`
@ -111,4 +116,31 @@ extension PureXML.Schema.ParticleRestriction {
}
return current == base
}
/// As ``typeDerivesOrEqual``, but the user-declared chain is followed ONLY across
/// `restriction` steps (an `extension` step breaks the walk), so a type derived by
/// extension does not count as a restriction of its base. Built-in derivation is
/// always by restriction, so the lattice continuation is unchanged. A `restriction`
/// of a union (declared `<restriction base="thatUnion">`) is reached through the
/// chain; an unrelated union with subset members is not. The ur-type base stays
/// permissive (any type restricts the ur-type), matching ``typeDerivesOrEqual``.
static func typeDerivesByRestriction(
_ derived: String,
_ base: String,
_ derivation: [String: PureXML.Schema.TypeDerivation],
_ types: [String: PureXML.Schema.ElementType],
) -> Bool {
if isUrTypeName(base, types) { return true }
var current = derived
var visited: Set<String> = []
while visited.insert(current).inserted {
if current == base { return true }
guard let step = derivation[current], step.method == .restriction else { break }
current = step.base
}
if let derivedBuiltin = PureXML.Schema.BuiltinType(rawValue: current), let baseBuiltin = PureXML.Schema.BuiltinType(rawValue: base) {
return derivedBuiltin.derives(from: baseBuiltin)
}
return current == base
}
}

View File

@ -0,0 +1,44 @@
import Testing
@testable import PureXML
/// XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2 (#158): when a complexContent
/// restriction narrows an element, the element's type must be validly derived from
/// the base element's type EXCLUDING extension (by restriction only). A type derived
/// from the base type by extension is not a valid restriction (corpus particlesIj008).
@Suite("XSD NameAndTypeOK restriction-only type derivation (#158)")
struct SchemaParticleTypeRestrictionTests {
private func compiles(_ derivedC1Type: String) -> Bool {
let schema = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="foo"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:complexType>
<xs:complexType name="barExt">
<xs:complexContent><xs:extension base="foo"><xs:sequence><xs:element name="b" type="xs:string"/></xs:sequence></xs:extension></xs:complexContent>
</xs:complexType>
<xs:complexType name="barRes">
<xs:complexContent><xs:restriction base="foo"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:restriction></xs:complexContent>
</xs:complexType>
<xs:complexType name="B"><xs:sequence><xs:element name="c1" type="foo"/></xs:sequence></xs:complexType>
<xs:complexType name="R">
<xs:complexContent><xs:restriction base="B"><xs:sequence><xs:element name="c1" type="\(derivedC1Type)"/></xs:sequence></xs:restriction></xs:complexContent>
</xs:complexType>
<xs:element name="doc" type="R"/>
</xs:schema>
"""
return (try? PureXML.Schema.Document(schema)) != nil
}
@Test("restricting an element to a type derived by EXTENSION is rejected (particlesIj008)")
func test_extensionDerivedTypeRejected() {
#expect(!compiles("barExt"))
}
@Test("restricting an element to a type derived by RESTRICTION is accepted")
func test_restrictionDerivedTypeAccepted() {
#expect(compiles("barRes"))
}
@Test("restricting an element to the same type is accepted")
func test_sameTypeAccepted() {
#expect(compiles("foo"))
}
}

View File

@ -146,9 +146,9 @@ struct XSTSSuiteTests {
/// model-group occurrence, identity-constraint content, derivation base kind,
/// attribute/simple-type rules, composition, simpleContent inline-type checks,
/// complexContent mixedness, and substitution-group anySimpleType checks.
/// Substitution barriers and element-ref metadata 45 -> 43; src-resolve unresolvable foreign-ref (#185) 43 -> 39; xsi: Not Allowed attr decls (#183) 39 -> 37.
/// element-ref metadata 45->43; src-resolve foreign-ref (#185) 43->39; xsi:Not-Allowed attrs (#183) 39->37; NameAndTypeOK restriction-only (#158) 37->36.
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 37
private let knownSchemaInvalidAccepted = 36
private let knownInstanceValidRejected = 0
/// 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