fix(schema): a reference to an all group must have maxOccurs 1
XSD 1.0 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. The direct `<all>` element was already checked by allGroupLimitedErrors, but a `<group ref>` resolving to an all group was not, so corpus particlesEa025 (`<group ref="x:G" maxOccurs="2">` to an all group) was accepted. Add allGroupReferenceMaxOccursErrors: collect the names of group definitions whose content is an all group, then flag a group reference to one whose maxOccurs is not 1 or whose minOccurs is not 0/1. The check runs over a single schema document tree, where group names are unique, so the by-local-name match cannot collide; a cross-document reference lives in a separate container and is under-rejected (never falsely rejected). XSTS invalid-schemas-accepted 36 -> 35; valid-schemas-rejected held at 0.
This commit is contained in:
parent
ee676cb470
commit
ca7a7d1433
@ -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 `<group ref>` that
|
||||
resolves to an `<xs:all>` group with `maxOccurs="2"` is now rejected (corpus
|
||||
particlesEa025); the direct `<all>` 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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 `<all>` element;
|
||||
/// this covers a `<group ref>` resolving to an all group (corpus particlesEa025).
|
||||
static func allGroupReferenceMaxOccursErrors(_ schema: XSDTree) -> [String] {
|
||||
let xsd = PureXML.Schema.XSDParser.xsdNamespace
|
||||
var allGroupNames: Set<String> = []
|
||||
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
|
||||
|
||||
43
Tests/SchemaAllGroupReferenceTests.swift
Normal file
43
Tests/SchemaAllGroupReferenceTests.swift
Normal file
@ -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 {
|
||||
"""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:group name="G"><xs:all><xs:element name="a1" type="xs:string"/><xs:element name="a2" type="xs:string"/></xs:all></xs:group>
|
||||
<xs:element name="doc"><xs:complexType><xs:group ref="t:G" maxOccurs="\(maxOccurs)"/></xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
}
|
||||
|
||||
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 = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:group name="G"><xs:sequence><xs:element name="a1" type="xs:string"/></xs:sequence></xs:group>
|
||||
<xs:element name="doc"><xs:complexType><xs:group ref="t:G" maxOccurs="2"/></xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(compiles(schema))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user