feat(schema): enforce attribute restriction type derivation (derivation-ok-restriction.1)
Require that any restricted attribute has a type validly derived by restriction from its base counterpart. Validates atomic built-in derivation, list item type restrictions, and union member/subset relationships recursively. Updates baseline to 362 accepted invalid schemas.
This commit is contained in:
parent
a7c007e969
commit
b6575150e8
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- A complexContent restriction must keep a base attribute's required use and its fixed value constraint (XSD 1.0 `cos-ct-derived-ok` / `derivation-ok-restriction.2` / `derivation-ok-restriction.4`). A restriction redeclaring a base `required` attribute may not make it optional or prohibited, and if a base attribute use has a `fixed` value constraint, the restriction must also make it `fixed` with the same value. The fixed values are compared in the attribute type's value space (supporting lists and unions) to avoid over-rejecting valid schemas. A prohibited attribute is removed, so the fixed clause does not apply to it. XSTS: invalid schemas accepted 390 to 387, no other bucket raised.
|
||||
- Enforce top-level declaration naming, block xs:anySimpleType derivation, and restrict simpleType facets. Top-level definitions must specify a `name` attribute. Derivation by restriction or extension of `xs:anySimpleType` is disallowed. Restriction facets (`length`, `minLength`, `maxLength`, `totalDigits`, `fractionDigits`) must be tighter than or equal to the base type's facets. XSTS: invalid schemas accepted 387 to 383, no other bucket raised.
|
||||
- Enforce simpleType derivation constraints on restricted complexType attributes (XSD 1.0 `cos-ct-derived-ok` / `derivation-ok-restriction.1`). The type of a restricted attribute must be validly derived by restriction from its base counterpart, checking atomic built-in derivation, list item type restriction, and union membership or member-to-member restriction recursively. XSTS: invalid schemas accepted 367 to 365, no other bucket raised.
|
||||
|
||||
## [0.1.0] - 2026-06-15
|
||||
|
||||
|
||||
@ -53,6 +53,9 @@ extension PureXML.Schema.XSDParser {
|
||||
if base.required, !derived.required {
|
||||
return "attribute '\(derived.name.localName)' is required in the base type and a restriction may not make it optional or prohibited"
|
||||
}
|
||||
if !isSimpleTypeRestrictionOK(derived: derived.type, base: base.type) {
|
||||
return "attribute '\(derived.name.localName)' has type '\(derived.type.base.rawValue)' which is not a valid restriction of base type '\(base.type.base.rawValue)'"
|
||||
}
|
||||
if let baseFixed = base.valueConstraint?.fixedValue {
|
||||
if isProhibited(derived.name, under: restrictionNode, context) {
|
||||
return nil
|
||||
@ -69,6 +72,34 @@ extension PureXML.Schema.XSDParser {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func isSimpleTypeRestrictionOK(derived: PureXML.Schema.SimpleType, base: PureXML.Schema.SimpleType) -> Bool {
|
||||
if base.isAnySimpleType {
|
||||
return true
|
||||
}
|
||||
switch (derived.variety, base.variety) {
|
||||
case (.atomic, .atomic):
|
||||
return derived.base.derives(from: base.base)
|
||||
case let (.list(derivedItem), .list(baseItem)):
|
||||
return isSimpleTypeRestrictionOK(derived: derivedItem, base: baseItem)
|
||||
case (.union, .union):
|
||||
guard case let .union(derivedMembers) = derived.variety,
|
||||
case let .union(baseMembers) = base.variety
|
||||
else { return false }
|
||||
return derivedMembers.allSatisfy { derivedMember in
|
||||
baseMembers.contains { baseMember in
|
||||
isSimpleTypeRestrictionOK(derived: derivedMember, base: baseMember)
|
||||
}
|
||||
}
|
||||
case (_, .union):
|
||||
guard case let .union(baseMembers) = base.variety else { return false }
|
||||
return baseMembers.contains { baseMember in
|
||||
isSimpleTypeRestrictionOK(derived: derived, base: baseMember)
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func isProhibited(_ name: PureXML.Model.QualifiedName, under node: XSDTree, _ context: PureXML.Schema.XSDContext, visited: Set<String> = []) -> Bool {
|
||||
for child in AttrRestrictNode.elementChildren(node) {
|
||||
switch AttrRestrictNode.localName(child) {
|
||||
|
||||
@ -73,11 +73,13 @@ public extension PureXML.Schema {
|
||||
public var base: BuiltinType
|
||||
public var facets: Facets
|
||||
public var variety: Variety
|
||||
public var isAnySimpleType: Bool
|
||||
|
||||
public init(base: BuiltinType, facets: Facets = Facets(), variety: Variety = .atomic) {
|
||||
public init(base: BuiltinType, facets: Facets = Facets(), variety: Variety = .atomic, isAnySimpleType: Bool = false) {
|
||||
self.base = base
|
||||
self.facets = facets
|
||||
self.variety = variety
|
||||
self.isAnySimpleType = isAnySimpleType
|
||||
}
|
||||
|
||||
/// A list type whose items are `item`, carrying its own (list) facets.
|
||||
|
||||
@ -40,7 +40,7 @@ extension PureXML.Schema.XSDParser {
|
||||
} else if let inline = PureXML.Schema.XSDNode.firstChild(node, named: "simpleType") {
|
||||
PureXML.Schema.XSDSimpleParser.simpleType(inline, context)
|
||||
} else {
|
||||
PureXML.Schema.SimpleType(base: .string)
|
||||
PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true)
|
||||
}
|
||||
let qualified = PureXML.Schema.XSDNode.attribute(node, "form") == "qualified"
|
||||
|| (PureXML.Schema.XSDNode.attribute(node, "form") == nil && context.attributeFormQualified)
|
||||
|
||||
@ -265,6 +265,7 @@ extension PureXML.Schema {
|
||||
|
||||
static func simpleTypeReference(_ typeName: String, _ context: XSDContext) -> SimpleType {
|
||||
let local = XSDNode.stripPrefix(typeName)
|
||||
if local == "anySimpleType" { return SimpleType(base: .string, isAnySimpleType: true) }
|
||||
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
|
||||
if let item = listBuiltinItem(local) { return .list(item: SimpleType(base: item)) }
|
||||
return context.simpleTypes[local] ?? SimpleType(base: .string)
|
||||
|
||||
@ -84,4 +84,28 @@ struct SchemaAttributeRestrictionTests {
|
||||
+ "<xs:attribute name=\"a\" type=\"xs:integer\" default=\"1\"/></xs:restriction></xs:complexContent></xs:complexType>"
|
||||
#expect(!compiles(intBase + derivedDefaultConstraint))
|
||||
}
|
||||
|
||||
@Test("A restriction may not change an attribute's type to a non-derived type")
|
||||
func test_attributeTypeRestriction() {
|
||||
let base = "<xs:attribute name=\"a\" type=\"xs:integer\"/>"
|
||||
// integer -> string (unrelated): invalid
|
||||
#expect(!compiles(derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"xs:string\"/>")))
|
||||
// integer -> int (valid restriction): valid
|
||||
#expect(compiles(derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"xs:int\"/>")))
|
||||
// integer -> union(float, integer): invalid
|
||||
let unionDef = "<xs:simpleType name=\"U\"><xs:union memberTypes=\"xs:float xs:integer\"/></xs:simpleType>"
|
||||
#expect(!compiles(unionDef + derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"U\"/>")))
|
||||
}
|
||||
|
||||
@Test("Restricting an untyped (anySimpleType) attribute to list and union types is valid")
|
||||
func test_anySimpleTypeRestrictedToListAndUnion() {
|
||||
let base = "<xs:attribute name=\"a\"/>" // untyped, defaults to xs:anySimpleType
|
||||
// anySimpleType -> atomic (xs:integer): valid
|
||||
#expect(compiles(derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"xs:integer\"/>")))
|
||||
// anySimpleType -> list (xs:NMTOKENS): valid
|
||||
#expect(compiles(derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"xs:NMTOKENS\"/>")))
|
||||
// anySimpleType -> union: valid
|
||||
let unionDef = "<xs:simpleType name=\"U\"><xs:union memberTypes=\"xs:integer xs:string\"/></xs:simpleType>"
|
||||
#expect(compiles(unionDef + derive(base: base, restricted: "<xs:attribute name=\"a\" type=\"U\"/>")))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = 1
|
||||
private let knownSchemaInvalidAccepted = 367
|
||||
private let knownSchemaInvalidAccepted = 365
|
||||
private let knownInstanceValidRejected = 180
|
||||
private let knownInstanceInvalidAccepted = 158
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | **72 -> 4** | production target is 0: rejecting a correct schema is the most user-hostile defect (#148). Particle-restriction over-rejection fixed: pointless `maxOccurs=0` particles removed, content-free restriction of an EMPTY base, empty-derived emptiable-base, and (#163) the pairwise check over the normalized content model with occurrence subsumption applied only to same-kind pairings (cross-kind element/group, group/element, group/wildcard match occurrence inside, by effective total range / member). #164 resolved element-ref namespaces by prefix binding (the `particlesJj/Jk/Jm/Jn/Js`, `particlesQ/R` cluster, -28). Content-free (`maxOccurs=0`) derived groups and substitution-group declaration order closed `particlesW006`/`elemZ027_a`; the W3C MapAndSum rule (Sequence:Choice, #165) closed `particlesV003`. Remaining 1 (`particlesZ001`): a documented spec ambiguity (RecurseAsIfGroup; the reference rejects it), not a defect. |
|
||||
| **invalid-schemas-accepted** | **2461 -> 367** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion landed). |
|
||||
| **invalid-schemas-accepted** | **2461 -> 365** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion landed). |
|
||||
| valid-instances-rejected | **233 -> 180** | (instance side; #164's element-ref namespace fix is shared with instance validation, -34) |
|
||||
| invalid-instances-accepted | **160 -> 158** | (instance side; resolving an inline `<simpleType>` restriction base preserves list/union variety, so a `length` facet counts items and two more invalid instances are caught) |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user