fix(schema): an untyped substitutionGroup member inherits its head's type

XSD 1.0 3.3.2: an element with neither a `type` attribute nor an inline type, but
a `substitutionGroup`, takes the {type definition} of its head. PureXML defaulted
such an element to `anyType`, so an instance of it accepted any content.
`XSDParser.elementType` now resolves the head declaration through
`context.globalElements` (transitively, with a cycle guard) and uses its type.

XSTS invalid-instances-accepted 29 -> 28 (typeDef00204m: <root>Yes</root> is no
longer accepted where root substitutes a boolean head), no other bucket moved
(valid-schemas-rejected and valid-instances-rejected held at 0 across all 14383
groups). 1451 unit tests pass.
This commit is contained in:
Mihaela Mihaljevic 2026-06-20 02:25:04 +02:00
parent 6927eb26f1
commit 45f2e7dbf5
5 changed files with 45 additions and 9 deletions

View File

@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- 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
declaration (transitively, guarding a `substitutionGroup` cycle) and uses its
type, so an instance of such a member is validated against the head's type. XSTS
invalid-instances-accepted 29 -> 28 (`typeDef00204m`), no other bucket moved
(valid-rejected held at 0 across all 14383 groups).
- `xsi:type` naming `anyType` is now rejected on an element whose declared type is
`anySimpleType` (cvc-elt.4.3.2.1). `anyType` is the ur-supertype of every type,
so it is never validly *derived* from `anySimpleType` (its own subtype). The

View File

@ -11,7 +11,11 @@ private typealias SimpleType = PureXML.Schema.SimpleType
extension PureXML.Schema.XSDParser {
// MARK: Element and type references
static func elementType(_ node: PureXML.Model.TreeNode, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ElementType {
static func elementType(
_ node: PureXML.Model.TreeNode,
_ context: PureXML.Schema.XSDContext,
resolvingHeads: Set<String> = [],
) -> PureXML.Schema.ElementType {
if let typeName = XSDNode.attribute(node, "type") {
return typeReference(typeName, context, at: node)
}
@ -21,6 +25,16 @@ extension PureXML.Schema.XSDParser {
if let inline = XSDNode.firstChild(node, named: "complexType") {
return .complex(complexType(inline, context))
}
// XSD 1.0 3.3.2: an element with neither `type` nor an inline type, but a
// `substitutionGroup`, takes the {type definition} of its head. Resolve the
// head declaration and use its type, guarding against a substitutionGroup cycle.
if let headReference = XSDNode.attribute(node, "substitutionGroup") {
let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings)
let headKey = derivationKey(XSDNode.stripPrefix(headReference), in: XSDNode.referenceNamespace(headReference, bindings))
if !resolvingHeads.contains(headKey), let headNode = context.globalElements[headKey] {
return elementType(headNode, context, resolvingHeads: resolvingHeads.union([headKey]))
}
}
return .complex(anyType)
}

View File

@ -264,4 +264,18 @@ struct SchemaSubstitutionTypeTests {
+ "</xs:element>",
))
}
@Test("an untyped substitutionGroup member inherits its head's type for instance validation")
func test_untypedMemberInheritsHeadType() throws {
let schema = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Head" type="xs:boolean"/>
<xs:element name="root" substitutionGroup="Head"/>
</xs:schema>
""")
// `root` has no type of its own but substitutes `Head` (boolean), so it
// inherits boolean: a valid boolean is accepted, a non-boolean rejected.
#expect(try schema.validate("<root>true</root>").isEmpty)
#expect(try !schema.validate("<root>Yes</root>").isEmpty)
}
}

View File

@ -150,13 +150,13 @@ struct XSTSSuiteTests {
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 43
private let knownInstanceValidRejected = 0
/// Instance bucket (133 -> 29), per-step deltas in CHANGELOG.md: xsi:type must derive from the
/// declared type (complex/atomic/list/union via the element-ref chain), and anyType cannot stand
/// in for an anySimpleType element (its supertype); block/blockDefault bar substitutions on the
/// namespace-keyed backbone; ur-type anyType wildcards are `lax`; identity fields compare in
/// decimal value space and take a declared default/fixed when absent/empty; an attribute wildcard
/// is the INTERSECTION of its sources; dateTime/date/gYear* reject year 0000.
private let knownInstanceInvalidAccepted = 29
/// Instance bucket (133 -> 28), 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
/// absent/empty; an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid.
private let knownInstanceInvalidAccepted = 28
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {

View File

@ -92,7 +92,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 |
| Type derivation | `typedef00204m/typeDef00204m1_n` | `ComplexValidatorXSIType`, derivation tables |
| 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` |
| Element constraints | `elemO011/elemO011.i`, `elemT074/elemT074.i` | instance validator, element declarations |