fix(schema): inherit finalDefault as a simple type's final (#145)

A simple type that declares no `final` of its own takes the schema's
`finalDefault` as its `{final}` property (XSD 1.0 Schemas section 3.14.2). The
list and union derivation checks read the `final` attribute directly but
ignored `finalDefault`, so `finalDefault="list"` or `"union"` did not block a
list item type or union member type and the schema was wrongly accepted.

The final map now falls back to `finalDefault` for a type without its own
`final`; an explicit `final=""` still overrides the default back to no
restriction, and a built-in item or member type stays unaffected (it is matched
only when the reference resolves to the schema's own namespace).

Caught two invalid schemas: XSTS invalid-schemas-accepted 217 -> 215, no other
bucket moved.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 13:59:19 +02:00
parent 50c6fb9b1c
commit 8420c7e2aa
4 changed files with 36 additions and 2 deletions

View File

@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
### Fixed
- XSTS invalid-schemas-accepted (#145): a simple type without its own `final` attribute now inherits the schema's `finalDefault` as its `{final}` property (XSD 1.0 Schemas §3.14.2), so `finalDefault="list"` or `"union"` blocks the matching list-item or union-member derivation just as an explicit `final` would. An explicit `final=""` still overrides the default back to no restriction. Caught two invalid schemas: invalid-schemas-accepted 217 → 215, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a `pattern` facet whose regular expression ends mid-token is now rejected as a schema-validity error. A trailing `\` with no character to escape (`a\`) raises `RegexError.incompleteEscape`, and a `[` character class the expression ends before closing with `]` (`a[`, `[a[:xyz:`) raises `RegexError.unterminatedClass`; both are unambiguously invalid rather than an engine limitation on an otherwise-valid pattern, so they reject. Caught thirteen invalid schemas: invalid-schemas-accepted 230 → 217, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): an attribute use that references a global attribute carrying a `fixed` value constraint may not contradict it. A `fixed` on the reference whose value differs from the global's (compared in the attribute type's value space, so `1` and `01` are not wrongly flagged), or a `default` on a reference to a fixed global, is invalid (`au-props-correct.2`). A reference that omits its own value constraint inherits the global's fixed value and stays valid. Caught two invalid schemas: invalid-schemas-accepted 232 → 230, no other bucket moved.

View File

@ -39,11 +39,20 @@ extension PureXML.Schema.XSDParser {
}
/// Each named simple type mapped to the derivation tokens its `final` forbids.
/// A type without its own `final` inherits the schema's `finalDefault` (its
/// `{final}` property; XSD 1.0 Schemas §3.14.2), so `finalDefault="list"`
/// makes every such type final for list derivation. An explicit `final=""`
/// still overrides the default back to no restriction.
private static func simpleTypeFinalMap(_ schema: XSDTree) -> [String: Set<String>] {
let fallback = finalTokens(FinalNode.attribute(schema, "finalDefault"))
var finalOf: [String: Set<String>] = [:]
for simpleType in descendants(schema, named: "simpleType") {
guard let name = FinalNode.attribute(simpleType, "name") else { continue }
finalOf[name] = finalTokens(FinalNode.attribute(simpleType, "final"))
if let own = FinalNode.attribute(simpleType, "final") {
finalOf[name] = finalTokens(own)
} else {
finalOf[name] = fallback
}
}
return finalOf
}

View File

@ -46,6 +46,25 @@ struct SchemaSimpleTypeFinalTests {
#expect(compiles(plain + unionOfT))
}
/// A type without its own `final` inherits the schema's `finalDefault` as its
/// `{final}`, so `finalDefault="list"`/`"union"` forbids the matching derivation
/// just as an explicit `final` would. An explicit `final=""` overrides it back.
@Test("finalDefault is inherited as the type's final")
func test_finalDefaultInherited() {
func compilesWith(_ finalDefault: String, _ body: String) -> Bool {
(try? PureXML.Schema.Document("<xs:schema \(xsd) finalDefault=\"\(finalDefault)\">\(body)</xs:schema>")) != nil
}
let plain = "<xs:simpleType name=\"T\"><xs:restriction base=\"xs:string\"/></xs:simpleType>"
let empty = "<xs:simpleType name=\"T\" final=\"\"><xs:restriction base=\"xs:string\"/></xs:simpleType>"
#expect(!compilesWith("list", plain + listOfT))
#expect(!compilesWith("union", plain + unionOfT))
// finalDefault names only one direction; the other stays open.
#expect(compilesWith("list", plain + unionOfT))
#expect(compilesWith("union", plain + listOfT))
// An explicit empty final overrides the default back to no restriction.
#expect(compilesWith("list", empty + listOfT))
}
/// A user type sharing a built-in's local name (here `string`, `final="list"`)
/// must not be confused with a reference to the built-in `xs:string`: a list of
/// `xs:string` is valid because the built-in's final is empty. The reference is

View File

@ -96,8 +96,12 @@ struct XSTSSuiteTests {
/// closed with `]`), both unambiguously invalid and never an engine
/// limitation, then caught thirteen more invalid schemas: invalid-schemas
/// accepted 230 → 217, no other bucket moved.
/// Inheriting the schema's `finalDefault` as a simple type's `{final}` when the
/// type declares no `final` of its own (so `finalDefault="list"`/`"union"`
/// blocks the matching list/union derivation) then caught two more invalid
/// schemas: invalid-schemas accepted 217 → 215, no other bucket moved.
private let knownSchemaValidRejected = 1
private let knownSchemaInvalidAccepted = 217
private let knownSchemaInvalidAccepted = 215
private let knownInstanceValidRejected = 3
private let knownInstanceInvalidAccepted = 132