diff --git a/CHANGELOG.md b/CHANGELOG.md index a21c4fc..77ca068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A reference to a model group whose content is an `all` group must have `maxOccurs` + of 1 and `minOccurs` of 0 or 1 (XSD 1.0 cos-all-limited, #158). A `` that + resolves to an `` group with `maxOccurs="2"` is now rejected (corpus + particlesEa025); the direct `` element was already checked. XSTS + invalid-schemas-accepted 36 -> 35, valid-schemas-rejected held at 0. + - 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 diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index bb2f520..32a7d09 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -66,7 +66,7 @@ extension PureXML.Schema.XSDParser { append(topLevelDeclarationErrors(schema), at: schema, into: &findings) append(nestedNamedDefinitionErrors(schema), at: schema, into: &findings) append(anySimpleTypeRestrictionErrors(schema) + anySimpleTypeFacetErrors(schema), at: schema, into: &findings) - append(emptyNamespaceErrors(schema), at: schema, into: &findings) + append(emptyNamespaceErrors(schema) + allGroupReferenceMaxOccursErrors(schema), at: schema, into: &findings) return findings } diff --git a/Sources/Schema/SchemaStructureHelpers.swift b/Sources/Schema/SchemaStructureHelpers.swift index 28fa3a4..7e73a0e 100644 --- a/Sources/Schema/SchemaStructureHelpers.swift +++ b/Sources/Schema/SchemaStructureHelpers.swift @@ -76,6 +76,38 @@ extension PureXML.Schema.XSDParser { return errors } + /// cos-all-limited: a reference to a model group whose content is an `all` group + /// is itself an all-group particle, so its `maxOccurs` must be 1 and its + /// `minOccurs` 0 or 1. ``allGroupLimitedErrors`` covers the direct `` element; + /// this covers a `` resolving to an all group (corpus particlesEa025). + static func allGroupReferenceMaxOccursErrors(_ schema: XSDTree) -> [String] { + let xsd = PureXML.Schema.XSDParser.xsdNamespace + var allGroupNames: Set = [] + for group in descendants(schema, named: "group") where group.name?.namespaceURI == xsd { + guard let name = PureXML.Schema.XSDNode.attribute(group, "name") else { continue } + let isAll = PureXML.Schema.XSDNode.elementChildren(group).contains { + $0.name?.namespaceURI == xsd && PureXML.Schema.XSDNode.localName($0) == "all" + } + if isAll { allGroupNames.insert(name) } + } + guard !allGroupNames.isEmpty else { return [] } + var errors: [String] = [] + for group in descendants(schema, named: "group") where group.name?.namespaceURI == xsd { + guard let ref = PureXML.Schema.XSDNode.attribute(group, "ref"), + allGroupNames.contains(PureXML.Schema.XSDNode.stripPrefix(ref)) + else { continue } + let maxOccurs = PureXML.Schema.XSDNode.attribute(group, "maxOccurs")?.trimmingXMLWhitespace() + if let maxOccurs, maxOccurs == "unbounded" || (isNonNegativeInteger(maxOccurs) && canonicalMagnitude(maxOccurs) != "1") { + errors.append("the maxOccurs of a reference to an all group must be 1") + } + let minOccurs = PureXML.Schema.XSDNode.attribute(group, "minOccurs")?.trimmingXMLWhitespace() + if let minOccurs, isNonNegativeInteger(minOccurs), !["0", "1"].contains(canonicalMagnitude(minOccurs)) { + errors.append("the minOccurs of a reference to an all group must be 0 or 1") + } + } + return errors + } + static func exceeds(_ lhs: String, _ rhs: String) -> Bool { let left = canonicalMagnitude(lhs), right = canonicalMagnitude(rhs) return left.count != right.count ? left.count > right.count : left > right diff --git a/Tests/SchemaAllGroupReferenceTests.swift b/Tests/SchemaAllGroupReferenceTests.swift new file mode 100644 index 0000000..a7000ca --- /dev/null +++ b/Tests/SchemaAllGroupReferenceTests.swift @@ -0,0 +1,43 @@ +import Testing +@testable import PureXML + +/// XSD 1.0 cos-all-limited (#158): a reference to a model group whose content is an +/// `all` group is itself an all-group particle, so its `maxOccurs` must be 1 (and +/// `minOccurs` 0 or 1). Mirrors corpus particlesEa025 (a group ref to an all group +/// with maxOccurs="2"). +@Suite("XSD cos-all-limited group reference (#158)") +struct SchemaAllGroupReferenceTests { + private func allGroupRef(maxOccurs: String) -> String { + """ + + + + + """ + } + + private func compiles(_ schema: String) -> Bool { + (try? PureXML.Schema.Document(schema)) != nil + } + + @Test("a group reference to an all group with maxOccurs>1 is rejected (particlesEa025)") + func test_allGroupRefMaxOccursTwoRejected() { + #expect(!compiles(allGroupRef(maxOccurs: "2"))) + } + + @Test("a group reference to an all group with maxOccurs=1 is accepted") + func test_allGroupRefMaxOccursOneAccepted() { + #expect(compiles(allGroupRef(maxOccurs: "1"))) + } + + @Test("a group reference to a non-all (sequence) group with maxOccurs>1 is accepted") + func test_sequenceGroupRefMaxOccursTwoAccepted() { + let schema = """ + + + + + """ + #expect(compiles(schema)) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 7ba0b4d..a3a5677 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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. - /// 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. + /// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 36 + private let knownSchemaInvalidAccepted = 35 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