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