fix(schema): require a content derivation in simpleContent/complexContent (#145)

A complexContent or simpleContent's content model requires exactly one
restriction or extension. An empty <complexContent/> declares no derivation
and is now rejected as a structural error.

XSTS invalid-schemas-accepted 200 to 197, no other bucket moved.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 14:58:58 +02:00
parent 947278bb56
commit b0479fd329
3 changed files with 23 additions and 1 deletions

View File

@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
### Fixed
- XSTS invalid-schemas-accepted (#145): a `complexContent` or `simpleContent` must contain exactly one `restriction` or `extension`. An empty `<complexContent/>` (or `<simpleContent/>`), which declares no derivation, is now rejected as a structural error. Caught three invalid schemas: invalid-schemas-accepted 200 → 197, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a `pattern` facet whose `\p{...}` or `\P{...}` property name can be no XSD `charProp` (`IsCategory | IsBlock`) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefix `Is`, a non-block name carrying a non-letter (`\p{\L}`), or `Is` followed by a non-block character. A well-formed but unrecognised category or block name (such as an unknown `Is`-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute `ref` may not also carry `form` (the form comes from the referenced global declaration; `src-attribute.3.2`), and no attribute declaration may be named `xmlns` (that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved.

View File

@ -22,8 +22,21 @@ extension PureXML.Schema.XSDParser {
/// Only the `complexContent` context is checked, where a model group is part of
/// the model; a `simpleContent` derivation (facets, no model group) has a
/// different model and is left to its own rules.
/// The number of `restriction`/`extension` children of a `simpleContent` or
/// `complexContent`. Its content model requires exactly one, so zero (an empty
/// `<complexContent/>`) or more than one is a structural error.
private static func derivationCount(_ content: XSDTree) -> Int {
PureXML.Schema.XSDNode.elementChildren(content).count { child in
guard child.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(child) else { return false }
return local == "restriction" || local == "extension"
}
}
static func complexContentOrderErrors(_ complexContent: XSDTree) -> [String] {
var errors: [String] = []
if derivationCount(complexContent) != 1 {
errors.append("a complexContent must contain exactly one 'restriction' or 'extension'")
}
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
guard let local = PureXML.Schema.XSDNode.localName(derivation),
local == "restriction" || local == "extension"
@ -65,6 +78,9 @@ extension PureXML.Schema.XSDParser {
/// table but is invalid (the ctD family).
static func simpleContentOrderErrors(_ simpleContent: XSDTree) -> [String] {
var errors: [String] = []
if derivationCount(simpleContent) != 1 {
errors.append("a simpleContent must contain exactly one 'restriction' or 'extension'")
}
for derivation in PureXML.Schema.XSDNode.elementChildren(simpleContent) {
guard let local = PureXML.Schema.XSDNode.localName(derivation) else { continue }
let slots: [(members: Set<String>, max: Int?)]

View File

@ -125,8 +125,12 @@ struct XSTSSuiteTests {
/// followed by a non-block character: `\p{\L}`, `\p{Is}`) while staying lenient
/// on a well-formed but unrecognised block name then caught three more invalid
/// schemas: invalid-schemas accepted 203 → 200, no other bucket moved.
/// Requiring a `complexContent` or `simpleContent` to contain exactly one
/// `restriction` or `extension` (an empty `<complexContent/>` has none) then
/// caught three more invalid schemas: invalid-schemas accepted 200 → 197, no
/// other bucket moved.
private let knownSchemaValidRejected = 1
private let knownSchemaInvalidAccepted = 200
private let knownSchemaInvalidAccepted = 197
private let knownInstanceValidRejected = 3
private let knownInstanceInvalidAccepted = 132