From 6927eb26f16a40f9740eeaff711a5ec35a065b0b Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sat, 20 Jun 2026 01:48:43 +0200 Subject: [PATCH] fix(schema): reject xsi:type=anyType on an anySimpleType-declared element cvc-elt.4.3.2.1 requires an xsi:type to name a type DERIVED from the element's declared type. anyType is the ur-supertype of every type, so it is never derived from anySimpleType (its own subtype); an anyType override on an anySimpleType element is therefore invalid. The general not-derived check stays silent on ur-type declared types to avoid false positives, so this adds the narrow, provably-invalid anyType-for-anySimpleType pairing only, leaving anyType valid on an anyType-declared element (which resolves to a complex ur-type and is excluded). XSTS invalid-instances-accepted 30 -> 29 (stZ056), no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at 0 across all 14383 groups). 1450 unit tests pass. --- CHANGELOG.md | 8 ++++ .../Schema/ComplexValidatorDerivation.swift | 37 +++++++++++++++++++ Tests/SchemaListUnionXsiTypeTests.swift | 21 +++++++++++ Tests/XSTSSuiteTests.swift | 14 +++---- docs/plans/2026-06-18-false-negative-tail.md | 2 +- 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7110ee0..545eaf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `xsi:type` naming `anyType` is now rejected on an element whose declared type is + `anySimpleType` (cvc-elt.4.3.2.1). `anyType` is the ur-supertype of every type, + so it is never validly *derived* from `anySimpleType` (its own subtype). The + general not-derived check stays silent on ur-type declared types to avoid false + positives; this adds only the provably-invalid `anyType`-for-`anySimpleType` + pairing, leaving `anyType` valid on an `anyType`-declared element. XSTS + invalid-instances-accepted 30 -> 29 (`stZ056`), no other bucket moved. + - `dateTime`, `date`, `gYearMonth`, and `gYear` now reject the year `0000`. XSD 1.0 has no year zero (the year before `0001` is `-0001`), so `0000-01-01T00:00:00` and the other `0000` forms are not valid values. `DateTimeParser.parseYearOnly` diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift index f8e00e6..7bfc90a 100644 --- a/Sources/Schema/ComplexValidatorDerivation.swift +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -16,6 +16,7 @@ extension PureXML.Schema.ComplexValidator { ) -> PureXML.Validation.ValidationError? { if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked } if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived } + if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return anyTypeSub } if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion } if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { return abstractType } if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) } @@ -66,6 +67,42 @@ extension PureXML.Schema.ComplexValidator { ) } + /// cvc-elt.4.3.2.1 for an `anyType` `xsi:type`: `anyType` is the ur-supertype of + /// every type, so it is never DERIVED from `anySimpleType` (its own subtype). An + /// `anyType` override on an `anySimpleType`-declared element is therefore invalid. + /// The general not-derived check stays silent on ur-type declared types to avoid + /// false positives; this rejects only the provably-invalid anyType-for-anySimpleType + /// pairing. `anyType` still validly substitutes for an `anyType`-declared element, + /// which `declaredIsAnySimpleType` excludes (that resolves to a complex ur-type). + func anyTypeForAnySimpleTypeError( + declared: PureXML.Schema.ElementType, + child: PureXML.Model.Element, + at path: [PureXML.Validation.PathKey], + namespaceBindings: [String: String], + ) -> PureXML.Validation.ValidationError? { + guard let label = Self.xsiTypeName(child), + let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings), + case let .complex(substitute)? = Self.resolveNamedType(reference, in: types), + Self.isUrType(substitute), + declaredIsAnySimpleType(declared) + else { return nil } + return PureXML.Validation.ValidationError( + reason: "xsi:type '\(label)' is not validly derived from the anySimpleType declared type of '\(child.name.localName)'", + at: path, + ) + } + + /// Whether `declared` resolves to the simple ur-type `anySimpleType` itself, not + /// `anyType` (a complex ur-type), a list/union, or any more specific type. + private func declaredIsAnySimpleType(_ declared: PureXML.Schema.ElementType) -> Bool { + let resolved: PureXML.Schema.ElementType? = switch declared { + case .simple, .complex: declared + case let .typeReference(reference): resolvedDeclaredType(reference) + } + if case let .simple(type)? = resolved { return type.isAnySimpleType } + return false + } + /// Whether a list or union substitute may validly stand in for `declared`: only /// the ur-types (`anySimpleType`/`anyType`) admit one. A declared type that is /// itself a list or union, or an unresolvable reference, is also treated as diff --git a/Tests/SchemaListUnionXsiTypeTests.swift b/Tests/SchemaListUnionXsiTypeTests.swift index 28c99ef..7879786 100644 --- a/Tests/SchemaListUnionXsiTypeTests.swift +++ b/Tests/SchemaListUnionXsiTypeTests.swift @@ -113,4 +113,25 @@ struct SchemaListUnionXsiTypeTests { // Drr restricts Dr: validly derived, accepted. #expect(try schema.validate(doc("Drr")).isEmpty) } + + @Test("xsi:type=anyType cannot substitute for an anySimpleType-declared element") + func test_anyTypeForAnySimpleType() throws { + let xsi = "http://www.w3.org/2001/XMLSchema-instance" + let schema = try PureXML.Schema.Document(""" + + + + + + + """) + func root(_ type: String, _ value: String) -> String { + #"\#(value)"# + } + // anyType is anySimpleType's supertype, not derived from it: rejected, for that reason. + #expect(try schema.validate(root("xs:anyType", "x")).contains { $0.reason.contains("anySimpleType declared type") }) + // anySimpleType (reflexive) and a derived atomic type are valid substitutes. + #expect(try schema.validate(root("xs:anySimpleType", "x")).isEmpty) + #expect(try schema.validate(root("xs:int", "123")).isEmpty) + } } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index de87d9e..2476424 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 43 private let knownInstanceValidRejected = 0 - /// Instance bucket (133 -> 30), per-step deltas in CHANGELOG.md: xsi:type must derive from the - /// declared type (complex, then atomic and list/union, resolved through the element-ref chain); - /// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the - /// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`; - /// identity fields compare in the decimal value space and take a declared default/fixed when - /// absent/empty; an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid. - private let knownInstanceInvalidAccepted = 30 + /// Instance bucket (133 -> 29), per-step deltas in CHANGELOG.md: xsi:type must derive from the + /// declared type (complex/atomic/list/union via the element-ref chain), and anyType cannot stand + /// in for an anySimpleType element (its supertype); block/blockDefault bar substitutions on the + /// namespace-keyed backbone; ur-type anyType wildcards are `lax`; identity fields compare in + /// decimal value space and take a declared default/fixed when absent/empty; an attribute wildcard + /// is the INTERSECTION of its sources; dateTime/date/gYear* reject year 0000. + private let knownInstanceInvalidAccepted = 29 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws { diff --git a/docs/plans/2026-06-18-false-negative-tail.md b/docs/plans/2026-06-18-false-negative-tail.md index 6b46581..753f629 100644 --- a/docs/plans/2026-06-18-false-negative-tail.md +++ b/docs/plans/2026-06-18-false-negative-tail.md @@ -101,7 +101,7 @@ classification is wrong. | Known particle under-rejection fallout | `particlesZ001/particlesZ001.i` | `ParticleRestriction` | | Regex instance edges | `reT17/reT17.i`, `reT38/reT38.i`, `RegexTest_422/RegexTest_422.i`, `RegexTest_430/RegexTest_430.i`, `reZ006i/reZ006i.i` | `Sources/Regex/*`, `SimpleType` | | Schema composition instance fallout | `schA2/schA2.i`, `schA5/schA5.i`, `schA7/schA7.i`, `schU3/schU3.i`, `schU4/schU4.i`, `schU5/schU5.i` | schema composition and instance validator | -| Simple-type instance edge | `stZ056/stZ056.i` | `SimpleType` | +| Simple-type instance edge | ~~`stZ056/stZ056.i`~~ closed: `xsi:type=anyType` cannot substitute for an `anySimpleType` element (its supertype) | `ComplexValidatorDerivation` | | Wildcard instance matching | `wildZ013a/wildZ013a.i`, `wildZ013d/wildZ013d.i` | `ContentMatcher`, wildcard declaration lookup | ## Tasks (ordered)