From b37e97b5a9e51b9cd74ef4eefb280e997ab87158 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 14 Jun 2026 01:04:25 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 1 + Sources/Schema/SchemaStructure.swift | 15 ++++++++++++++- Tests/SchemaStructureTests.swift | 17 +++++++++++++++++ Tests/XSTSSuiteTests.swift | 2 +- docs/schema-validity-burndown.md | 4 ++-- docs/xsts-deviations.md | 4 ++-- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b0374..2f3f5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index bb4ceda..bcf21e8 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -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"] + } } diff --git a/Tests/SchemaStructureTests.swift b/Tests/SchemaStructureTests.swift index 68f45d0..9115426 100644 --- a/Tests/SchemaStructureTests.swift +++ b/Tests/SchemaStructureTests.swift @@ -163,6 +163,23 @@ struct SchemaStructureTests { try compile(#""#) } + @Test("a named group must contain exactly one model group") + func test_namedGroupContent() throws { + try compile(#""#) + // Two compositors, or none, is invalid. + #expect(rejects(#""" + + + + + """#)) + #expect(rejects(#"x"#)) + // A group reference (no name) is not a definition and is not flagged here. + try compile( + #""#, + ) + } + @Test("an identity constraint requires a selector and field") func test_identityConstraintRequiresParts() { #expect(rejects(#""" diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 17f6e3f..4c69291 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index 7d23851..4cb0727 100644 --- a/docs/schema-validity-burndown.md +++ b/docs/schema-validity-burndown.md @@ -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) diff --git a/docs/xsts-deviations.md b/docs/xsts-deviations.md index f339978..ace179d 100644 --- a/docs/xsts-deviations.md +++ b/docs/xsts-deviations.md @@ -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)