From ee676cb470730ac5ae4cd78aa3e97ed5a3622ae4 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sat, 20 Jun 2026 18:12:58 +0200 Subject: [PATCH] fix(schema): restriction may not narrow an element to an extension-derived type XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2: when a complexContent restriction narrows an element, the element's type must be validly derived from the base element's type EXCLUDING extension (and list/union), i.e. by restriction only. The element-type check followed the derivation chain regardless of method, so a type derived from the base by extension was wrongly accepted as a valid restriction (corpus particlesIj008). Add typeDerivesByRestriction (as typeDerivesOrEqual, but the user-declared chain is followed across restriction steps only; an extension step breaks the walk) and use it on the non-union fall-through of elementTypeRestrictionOK. The shared typeDerivesOrEqual is untouched, so substitution-group validity (where extension is legal) is unaffected. Built-in lattice and union-base derivation are unchanged, so valid restrictions, including the union-member restriction addB150, still compile. XSTS invalid-schemas-accepted 37 -> 36; valid-schemas-rejected held at 0 across all 14383 groups. Union-base (Ik011/Ik025/Ik027) and substitution-group (V020) cases need the by-name union-member and ref-resolution clauses and stay deferred. --- CHANGELOG.md | 7 +++ .../Schema/ParticleRestrictionElement.swift | 34 +++++++++++++- .../SchemaParticleTypeRestrictionTests.swift | 44 +++++++++++++++++++ Tests/XSTSSuiteTests.swift | 4 +- 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 Tests/SchemaParticleTypeRestrictionTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 433db59..a21c4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- 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 + chain across `restriction` steps only, so an extension-derived type is not a valid + restriction (corpus particlesIj008). Built-in lattice and union-base derivation are + unchanged. XSTS invalid-schemas-accepted 37 -> 36, valid-schemas-rejected held at 0. + - A `fixed` value on an element whose type is a complex type with `simpleContent` is now compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw string (#202), so `05` satisfies an `xs:int` `fixed="5"`. The complex-element fixed check on diff --git a/Sources/Schema/ParticleRestrictionElement.swift b/Sources/Schema/ParticleRestrictionElement.swift index 496d822..2be6d89 100644 --- a/Sources/Schema/ParticleRestrictionElement.swift +++ b/Sources/Schema/ParticleRestrictionElement.swift @@ -54,7 +54,12 @@ extension PureXML.Schema.ParticleRestriction { } if baseIsUnion(baseTypeName, types) { return true } - return typeDerivesOrEqual(restrictedTypeName, baseTypeName, derivation, types) + // NameAndTypeOK.2.2: a restricting element's type must be validly derived from + // the base element's type EXCLUDING extension (and list/union), i.e. by + // restriction only. A type that reaches the base through an extension step is + // not a valid restriction. (Union-base cases are still handled above; the + // by-name restriction-derivation of a union member is a separate clause.) + return typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) } /// Whether the named type resolves (through the type table) to a `union` @@ -111,4 +116,31 @@ extension PureXML.Schema.ParticleRestriction { } return current == base } + + /// As ``typeDerivesOrEqual``, but the user-declared chain is followed ONLY across + /// `restriction` steps (an `extension` step breaks the walk), so a type derived by + /// extension does not count as a restriction of its base. Built-in derivation is + /// always by restriction, so the lattice continuation is unchanged. A `restriction` + /// of a union (declared ``) is reached through the + /// chain; an unrelated union with subset members is not. The ur-type base stays + /// permissive (any type restricts the ur-type), matching ``typeDerivesOrEqual``. + static func typeDerivesByRestriction( + _ derived: String, + _ base: String, + _ derivation: [String: PureXML.Schema.TypeDerivation], + _ types: [String: PureXML.Schema.ElementType], + ) -> Bool { + if isUrTypeName(base, types) { return true } + var current = derived + var visited: Set = [] + while visited.insert(current).inserted { + if current == base { return true } + guard let step = derivation[current], step.method == .restriction else { break } + current = step.base + } + if let derivedBuiltin = PureXML.Schema.BuiltinType(rawValue: current), let baseBuiltin = PureXML.Schema.BuiltinType(rawValue: base) { + return derivedBuiltin.derives(from: baseBuiltin) + } + return current == base + } } diff --git a/Tests/SchemaParticleTypeRestrictionTests.swift b/Tests/SchemaParticleTypeRestrictionTests.swift new file mode 100644 index 0000000..91af692 --- /dev/null +++ b/Tests/SchemaParticleTypeRestrictionTests.swift @@ -0,0 +1,44 @@ +import Testing +@testable import PureXML + +/// XSD 1.0 cos-particle-restrict NameAndTypeOK.2.2 (#158): when a complexContent +/// restriction narrows an element, the element's type must be validly derived from +/// the base element's type EXCLUDING extension (by restriction only). A type derived +/// from the base type by extension is not a valid restriction (corpus particlesIj008). +@Suite("XSD NameAndTypeOK restriction-only type derivation (#158)") +struct SchemaParticleTypeRestrictionTests { + private func compiles(_ derivedC1Type: String) -> Bool { + let schema = """ + + + + + + + + + + + + + + + """ + return (try? PureXML.Schema.Document(schema)) != nil + } + + @Test("restricting an element to a type derived by EXTENSION is rejected (particlesIj008)") + func test_extensionDerivedTypeRejected() { + #expect(!compiles("barExt")) + } + + @Test("restricting an element to a type derived by RESTRICTION is accepted") + func test_restrictionDerivedTypeAccepted() { + #expect(compiles("barRes")) + } + + @Test("restricting an element to the same type is accepted") + func test_sameTypeAccepted() { + #expect(compiles("foo")) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index b4b40f9..7ba0b4d 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. - /// Substitution barriers and element-ref metadata 45 -> 43; src-resolve unresolvable foreign-ref (#185) 43 -> 39; xsi: Not Allowed attr decls (#183) 39 -> 37. + /// 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. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 37 + private let knownSchemaInvalidAccepted = 36 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