From aa6e76da889f4ce8b17fe4bb2a60f0d141148cdb Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 15 Jun 2026 14:06:26 +0200 Subject: [PATCH] feat(schema): validate value constraints against user types; fix inline restriction base A default/fixed value on an element or attribute whose type names a user-declared type in the schema's own target namespace must be a valid value of that type (XSD 1.0 e-props-correct.2 / a-props-correct.2). The check runs after the named types are compiled and validates with the compiled SimpleType validator (the one instance validation uses), so whitespace and facet handling match. Built-ins are handled at structure time; imported, foreign, and inline anonymous types are left alone via a namespace gate. This exposed a pre-existing compile defect: a simpleType restriction read its base only from the base attribute, so an inline anonymous base (a list or union with no base attribute) was dropped and the type collapsed to an atomic string, making a length facet count characters instead of list items. The restriction branch now resolves the inline child as its base, preserving list and union variety. This corrects both schema-time value checking and instance validation. XSTS: invalid schemas accepted 462 to 454; invalid instances accepted 160 to 158; no other bucket raised. --- CHANGELOG.md | 2 + Sources/Schema/SchemaFacetApplicability.swift | 49 ++++++++++ Sources/Schema/SchemaIDAttribute.swift | 6 +- Sources/Schema/XSDSimpleParser.swift | 13 ++- .../SchemaUserTypeValueConstraintTests.swift | 91 +++++++++++++++++++ Tests/XSTSSuiteTests.swift | 4 +- docs/schema-validity-burndown.md | 4 +- 7 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 Tests/SchemaUserTypeValueConstraintTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index de3997a..6a83723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A `default`/`fixed` value on an element or attribute whose `type` names a user-declared type must now be a valid value of that type (XSD 1.0 `e-props-correct.2` / `a-props-correct.2`, #170). This runs after the named types are compiled, complementing the existing built-in-only check: a `default` failing the type's `pattern`, a malformed value for a type restricting a built-in, or a value invalid against a complex type's simple content is now rejected. The type name is resolved namespace-aware; only a type in the schema's own target namespace is checked (built-ins are handled at structure time; imported, foreign, or inline anonymous types are left alone), and the compiled `SimpleType` validator (the one instance validation uses) performs the check, so whitespace and facet handling match. XSTS: invalid schemas accepted 462 to 454, no other bucket raised. +- A simple-type `restriction` whose base is an inline anonymous `` (no `base` attribute) now resolves that inline child as its base, preserving its list or union variety. The base was read only from the `base` attribute, so an inline list/union base was dropped and the type collapsed to an atomic string: a `length` facet then counted characters instead of list items, and a list-valued `default` of the correct length was wrongly rejected once value constraints are checked. Resolving the inline base fixes both schema-time value checking and instance validation. XSTS: invalid instances accepted 160 to 158, no other bucket raised. - An `xs:attribute` declaration carrying a `default` value constraint must now have `use="optional"` (XSD 1.0 `src-attribute.2`, #170). A default makes the attribute optional, so `use="required"` or `use="prohibited"` alongside it is contradictory; such schemas were accepted. The check rides the existing value-constraint walk, gated to XSD-namespace `attribute` declarations reading the unprefixed `default`/`use`, and is per declaration. Only `fixed` (not `default`) may pair with `use="required"`. XSTS: invalid schemas accepted 469 to 462, no other bucket raised. Disclosed under-rejection: a whitespace-padded `use=" required "` (whose NMTOKEN actual value is `required`) is not flagged. - An element or attribute declaration may no longer carry both a `default` and a `fixed` value constraint (XSD 1.0 `src-element.1` / `src-attribute.1`, #170). The two are mutually exclusive; such schemas were accepted. The check rides the existing structure walk, gated to XSD-namespace declarations and reading the unprefixed `default`/`fixed` (a foreign same-local-name attribute is not the value constraint), and is per declaration, so a `default` on a global declaration and a `fixed` on a reference to it (two separate items) is correctly allowed. XSTS: invalid schemas accepted 479 to 469, no other bucket raised. - A substitution-group member's type must now be validly derived from the type of its head (XSD 1.0 `e-props-correct.4`, #170). An element declaring `substitutionGroup="head"` whose own type is unrelated to the head's (for example a member typed `xs:int` under a head typed `xs:string`) was accepted; it is now rejected. The check runs after named types resolve and reuses the type-derivation oracle (the restriction/extension chain, continued through the built-in lattice; an ur-type head admits any member). To never reject a valid member it is conservative: it runs only for a self-contained schema (no `import`/`include`/`redefine`, where the derivation table is complete and single-namespace names are unambiguous), reads the member's type from its own `type` attribute and the head's from the top-level element declarations only (so a local element of the same name supplies neither), and stands down when either type is a list or union (the `cos-st-derived-ok` clauses 2.3/2.4 union-member derivation is not modelled). XSTS: invalid schemas accepted 480 to 479, no other bucket raised. Disclosed under-rejections: schemas that compose other documents, list/union-typed members or heads, and inline/untyped members. diff --git a/Sources/Schema/SchemaFacetApplicability.swift b/Sources/Schema/SchemaFacetApplicability.swift index 851132e..a1f2486 100644 --- a/Sources/Schema/SchemaFacetApplicability.swift +++ b/Sources/Schema/SchemaFacetApplicability.swift @@ -65,6 +65,55 @@ extension PureXML.Schema.XSDParser { return errors } + /// A `default`/`fixed` value on an `element` or `attribute` whose `type` names a + /// user-declared type in this schema's own target namespace must be a valid value + /// of that type (Element/Attribute Locally Valid, `e-props-correct.2` / + /// `a-props-correct.2`). This runs after the named types are compiled, so it + /// complements the built-in-only structure-time check: a `fixed="false"` on a + /// type restricting `xs:boolean` by `pattern="true"`, or a `default="Yes"` on a + /// complex type whose simple content extends `xs:boolean`, is now rejected. + /// + /// The type name is resolved namespace-aware; only a type in the schema's own + /// target namespace (looked up in the compiled `types`) is checked. A built-in + /// (handled at structure time), an imported or otherwise foreign type, or an + /// inline anonymous type is left alone, so no valid value constraint is rejected. + /// The compiled `SimpleType` validator is the same one instance validation uses, + /// so its whitespace and facet handling match. + static func userTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] { + let bindings = context.namespaceBindings + var errors: [String] = [] + forEachValueConstrained(schema) { node in + guard node.name?.namespaceURI == xsdNamespace, + let typeName = unprefixedValue(node, "type") + else { return } + let fixed = unprefixedValue(node, "fixed") + guard let value = fixed ?? unprefixedValue(node, "default") else { return } + let (prefix, local) = splitQName(typeName) + let uri = prefix.map { bindings[$0] } ?? bindings[""] + guard uri == context.targetNamespace, let resolved = types[local], + let simple = simpleContentType(of: resolved), !simple.isValid(value) + else { return } + errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'") + } + return errors + } + + /// The simple type a value constraint is validated against: a simple type + /// directly, or the simple content of a complex type. A complex type with + /// element-only, mixed, or empty content carries no simple value space here, so + /// it is not value-checked (a disclosed under-rejection). + private static func simpleContentType(of type: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? { + switch type { + case let .simple(simple): + return simple + case let .complex(complex): + if case let .simpleContent(simple) = complex.content { return simple } + return nil + case .typeReference: + return nil + } + } + private static func forEachValueConstrained(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) if local == "appinfo" || local == "documentation" { return } diff --git a/Sources/Schema/SchemaIDAttribute.swift b/Sources/Schema/SchemaIDAttribute.swift index ab0d3d9..63ece6e 100644 --- a/Sources/Schema/SchemaIDAttribute.swift +++ b/Sources/Schema/SchemaIDAttribute.swift @@ -55,8 +55,9 @@ extension PureXML.Schema.XSDParser { /// Consistency findings that depend on the resolved named types, collected after /// `namedTypes` has populated them: unresolved references, attribute-use - /// uniqueness and single-ID, ID-typed value constraints, and substitution-group - /// member type derivation (`e-props-correct.4`). + /// uniqueness and single-ID, ID-typed value constraints, substitution-group + /// member type derivation (`e-props-correct.4`), and value constraints valid + /// against a user type (`e-props-correct.2` / `a-props-correct.2`). static func postNamedTypeErrors( _ schema: XSDTree, _ context: PureXML.Schema.XSDContext, @@ -68,5 +69,6 @@ extension PureXML.Schema.XSDParser { + attributeUseErrors(containers, context) + idValueConstraintErrors(schema, context) + substitutionTypeErrors(schema, derivation, typeMaps.named) + + userTypeValueConstraintErrors(schema, context, typeMaps.named) } } diff --git a/Sources/Schema/XSDSimpleParser.swift b/Sources/Schema/XSDSimpleParser.swift index 4e1af9c..dd3c90e 100644 --- a/Sources/Schema/XSDSimpleParser.swift +++ b/Sources/Schema/XSDSimpleParser.swift @@ -16,8 +16,17 @@ extension PureXML.Schema { // Resolve the base through simpleTypeReference so the built-in list // datatypes (NMTOKENS, IDREFS, ENTITIES) keep their list variety: a // length facet on one of them counts list items, not characters (#146). - // Restricting a user type inherits its base, facets, and variety. - let baseType = simpleTypeReference(XSDNode.attribute(restriction, "base") ?? "string", context) + // Restricting a user type inherits its base, facets, and variety. When the + // `base` attribute is absent the base is the inline `` child + // (an anonymous base); resolving it likewise preserves its list/union + // variety, so a length facet over an inline list base counts items. + let baseType: SimpleType = if let baseName = XSDNode.attribute(restriction, "base") { + simpleTypeReference(baseName, context) + } else if let inlineBase = XSDNode.firstChild(restriction, named: "simpleType") { + simpleType(inlineBase, context) + } else { + SimpleType(base: .string) + } var facets = baseType.facets // A restriction's own `enumeration` replaces the inherited set (the new // enumeration is the value space, not a union with the base's), whereas diff --git a/Tests/SchemaUserTypeValueConstraintTests.swift b/Tests/SchemaUserTypeValueConstraintTests.swift new file mode 100644 index 0000000..b8b6fda --- /dev/null +++ b/Tests/SchemaUserTypeValueConstraintTests.swift @@ -0,0 +1,91 @@ +@testable import PureXML +import Testing + +@Suite("XSD value constraint valid against a user type (e-props-correct.2 / a-props-correct.2)") +struct SchemaUserTypeValueConstraintTests { + private let xsd = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + private func compiles(_ body: String) -> Bool { + (try? PureXML.Schema.Document("\(body)")) != nil + } + + @Test("A default invalid against a named simpleType's pattern is rejected") + func test_namedSimpleTypePatternRejected() { + #expect(!compiles( + "" + + "" + + "", + )) + } + + @Test("A fixed invalid for a named simpleType restricting a built-in is rejected") + func test_namedSimpleTypeLexicalRejected() { + #expect(!compiles( + "" + + "", + )) + } + + @Test("A fixed invalid against a complexType's simpleContent base is rejected") + func test_simpleContentExtensionRejected() { + #expect(!compiles( + "" + + "" + + "" + + "", + )) + } + + @Test("A value valid for its user type compiles") + func test_validValueAccepted() { + // Matches the pattern. + #expect(compiles( + "" + + "" + + "", + )) + // Valid float. + #expect(compiles( + "" + + "", + )) + // Valid against the simpleContent base. + #expect(compiles( + "" + + "" + + "", + )) + } + + /// A value valid for one of a union's member types must compile: the compiled + /// `SimpleType` validator admits any member, so no special-casing is needed. + @Test("A value valid for a union member compiles") + func test_unionMemberAccepted() { + #expect(compiles( + "" + + "", + )) + } + + /// A restriction whose base is an inline `` list must preserve the + /// list variety, so a `length` facet counts items and a list-valued default of + /// the right length is valid. Reading the base only from the `base` attribute + /// dropped the inline list, made the type an atomic string, and counted + /// characters, wrongly rejecting this schema. + @Test("A list-valued default on an inline-list-base restriction compiles") + func test_inlineListBaseRestrictionAccepted() { + #expect(compiles( + "" + + "" + + "" + + "", + )) + // The same type with a list of the wrong length is still correctly rejected. + #expect(!compiles( + "" + + "" + + "" + + "", + )) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 7ff73f8..5418a68 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -29,9 +29,9 @@ 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 = 462 + private let knownSchemaInvalidAccepted = 454 private let knownInstanceValidRejected = 180 - private let knownInstanceInvalidAccepted = 160 + private let knownInstanceInvalidAccepted = 158 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws { diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index ac1942e..3e4c330 100644 --- a/docs/schema-validity-burndown.md +++ b/docs/schema-validity-burndown.md @@ -10,9 +10,9 @@ 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 -> 462** | 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 -> 454** | 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 | | +| invalid-instances-accepted | **160 -> 158** | (instance side; resolving an inline `` restriction base preserves list/union variety, so a `length` facet counts items and two more invalid instances are caught) | One-sentence diagnosis: *the validator runs its instance-validity rules but not its schema-validity rules.* It compiles what it understands and ignores the