feat(schema): a named group must contain exactly one model group

XSD 1.0 gives a named model-group definition's content as
(annotation?, (all | choice | sequence)) -- exactly one compositor. The
allowedChildren set-membership check allowed any number, so an xs:group with two
compositors, or with none, was accepted. namedGroupErrors now enforces the
cardinality, gated on the node being a definition (has a name) so a group
reference is never flagged.

XSTS: invalid schemas accepted 977 to 956 (-21); valid schemas rejected held at
72; instance buckets unchanged.
This commit is contained in:
Mihaela Mihaljevic 2026-06-14 01:04:25 +02:00
parent 09a0f9bf5c
commit b37e97b5a9
6 changed files with 37 additions and 6 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A named model-group definition (`xs:group name="..."`) must contain exactly one of `all`/`choice`/`sequence` (#147). XSD 1.0 gives a group's content as `(annotation?, (all | choice | sequence))`, but the `allowedChildren` set-membership check did not enforce the cardinality, so a group with two compositors or with none was accepted. A `group` reference (no `name`) is excluded. XSTS: invalid schemas accepted 977 to 956, no other bucket raised.
- A particle whose `minOccurs` exceeds its `maxOccurs` is now rejected at compile time (#147). An occurrence range like `minOccurs="5" maxOccurs="2"` is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), with `unbounded` never exceeded and a single-point range (`minOccurs="2" maxOccurs="2"`) allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised.
- A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be `simpleContent`, or `complexContent`, or a model group followed by attributes, but never a mix: so at most one of `simpleContent`/`complexContent`, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. The `allowedChildren` set-membership check did not catch these combinations, so a malformed complexType (two `simpleContent`, `simpleContent` plus a `sequence`, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised.
- Unresolvable references are now rejected at compile time (#147). Every QName a schema names (a `type`/`base`/`itemType`/`memberTypes` type, an `element`/`attribute`/`group`/`attributeGroup` `ref`, an element `substitutionGroup`) must resolve to a declared component or a built-in; an undeclared reference was previously accepted and only surfaced (if at all) lazily during instance validation. The reference is matched by local name with whitespace trimmed, as the rest of the compiler resolves it. The check is skipped when the document pulls in external definitions through `import`/`include`/`redefine`, which the default compile does not load, so a reference into them is never flagged. XSTS: invalid schemas accepted 1092 to 1051, no other bucket raised.

View File

@ -66,10 +66,13 @@ extension PureXML.Schema.XSDParser {
if node.name?.namespaceURI == xsdNamespace {
errors += attributeValueErrors(node)
errors += occurrenceOrderErrors(node)
let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName)
if let local, let allowed = allowedChildren[local] {
let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName)
errors += childErrors(local: local, children: names, allowed: allowed)
}
if local == "group", PureXML.Schema.XSDNode.attribute(node, "name") != nil {
errors += namedGroupErrors(names)
}
}
for child in children {
collectStructure(child, into: &errors)
@ -219,4 +222,14 @@ extension PureXML.Schema.XSDParser {
}
return []
}
/// The finding, if any, for a named model-group definition (`xs:group` with a
/// `name`): its content is exactly one of `all`/`choice`/`sequence` (XSD 1.0
/// Structures), so neither an empty group nor one with two compositors is
/// valid. (A `group` *reference* carries no compositor child and is excluded
/// by the caller.)
private static func namedGroupErrors(_ children: [String]) -> [String] {
let compositors = children.count { $0 == "all" || $0 == "choice" || $0 == "sequence" }
return compositors == 1 ? [] : ["a named group must contain exactly one of all, choice, or sequence"]
}
}

View File

@ -163,6 +163,23 @@ struct SchemaStructureTests {
try compile(#"<xs:complexType name="t"><xs:attribute name="x" type="xs:string"/></xs:complexType>"#)
}
@Test("a named group must contain exactly one model group")
func test_namedGroupContent() throws {
try compile(#"<xs:group name="g"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:group>"#)
// Two compositors, or none, is invalid.
#expect(rejects(#"""
<xs:group name="g">
<xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence>
<xs:all><xs:element name="b" type="xs:string"/></xs:all>
</xs:group>
"""#))
#expect(rejects(#"<xs:group name="g"><xs:annotation><xs:documentation>x</xs:documentation></xs:annotation></xs:group>"#))
// A group reference (no name) is not a definition and is not flagged here.
try compile(
#"<xs:group name="g"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:group><xs:complexType name="t"><xs:group ref="g"/></xs:complexType>"#,
)
}
@Test("an identity constraint requires a selector and field")
func test_identityConstraintRequiresParts() {
#expect(rejects(#"""

View File

@ -29,7 +29,7 @@ struct XSTSSuiteTests {
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
/// are written to /tmp/xsts-failures.txt for the burn-down.
private let knownSchemaValidRejected = 72
private let knownSchemaInvalidAccepted = 977
private let knownSchemaInvalidAccepted = 956
private let knownInstanceValidRejected = 214
private let knownInstanceInvalidAccepted = 165

View File

@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
| Bucket | Count | Reading |
|---|---|---|
| valid-schemas-rejected | 72 | we rarely reject a good schema |
| **invalid-schemas-accepted** | **2461 -> 977** | we rarely catch a bad one (facet, id, and structural content-model validity landed) |
| **invalid-schemas-accepted** | **2461 -> 956** | we rarely catch a bad one (facet, id, and structural content-model validity landed) |
| valid-instances-rejected | 233 | (instance side, separately tracked) |
| invalid-instances-accepted | 165 | |
@ -75,7 +75,7 @@ Each iteration targets one self-contained rule family with a clean root cause.
(1688 -> 1313, -375)._ A data-driven `allowedChildren` table (the schema-for-
schemas child content model) rejects disallowed children, multi/misplaced
`annotation`, and identity constraints missing selector/field, via one general
mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997), plus **particle minOccurs <= maxOccurs** (997 -> 977). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness).
mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997), plus **particle minOccurs <= maxOccurs** (997 -> 977), plus **named-group exactly-one-compositor** (977 -> 956). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness).
## Per-iteration protocol (the PR-critic-loop)

View File

@ -47,7 +47,7 @@ and to document the remainder as spec-justified, deliberate exclusions.
## Category A: genuine correctness bugs
### Fixed (twenty-four root causes, ~2158 deviations cleared)
### Fixed (twenty-five root causes, ~2179 deviations cleared)
list-datatype length facets (#146) · union pattern/enumeration facets · duration
partial-order facets · QName length non-constraining · the full XSD `\p{Is...}`
@ -65,7 +65,7 @@ validity (`min`/`maxInclusive`, `min`/`maxExclusive`, `enumeration` values valid
in the base space; inclusive/exclusive exclusions; bound ordering) · `gMonth`
`--MM--` lexical form · schema `id` attribute validity (`xs:ID`: NCName and
unique within the document). The schema-validity work brought invalid schemas
accepted 2461 to 977 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`.
accepted 2461 to 956 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`.
Plus the measurement fix (exclude W3C-disputed `status="queried"` entries).
### Remaining, root-caused (the hard tail)