feat(schema): attribute default requires use=optional (src-attribute.2)
An xs:attribute declaration carrying a default value constraint was accepted alongside use="required" or use="prohibited"; XSD 1.0 src-attribute.2 requires use to be optional when default is present (a default makes the attribute optional). 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 " (NMTOKEN actual value "required") is not flagged.
This commit is contained in:
parent
de82800525
commit
a087c022e9
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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.
|
||||
- An `xs:all` group referenced through a `<group ref>` from inside a compositor is now rejected (XSD 1.0 `cos-all-limited.1.2`, #170): an `all` group may only be the whole content model of a complex type. A direct nested `all` was already rejected by the child-content table; this closes the group-reference path. The all-group pool is qualified by the defining schema's target namespace and a reference by its prefix, so an imported or redefined same-named group is not confused with a local one; a group redefined to a non-all content is exempt. XSTS: invalid schemas accepted 481 to 480, no other bucket raised.
|
||||
|
||||
@ -29,9 +29,10 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
|
||||
/// A `default`/`fixed` value on an `element` or `attribute` must be a valid value
|
||||
/// of its `type` (Attribute/Element Locally Valid, XSD 1.0), and the two are
|
||||
/// of its `type` (Attribute/Element Locally Valid, XSD 1.0); the two are
|
||||
/// mutually exclusive (`src-element.1` / `src-attribute.1`: `default` and `fixed`
|
||||
/// must not both be present). Only a type that resolves to a built-in datatype is
|
||||
/// must not both be present); and an `attribute` carrying a `default` must have
|
||||
/// `use="optional"` (`src-attribute.2`). Only a type that resolves to a built-in datatype is
|
||||
/// value-checked, since the check runs before the types are compiled; a
|
||||
/// `fixed="abc"` on an `xs:integer` was wrongly accepted. A named or inline user
|
||||
/// type, or a type in another namespace, is left alone, so no valid value
|
||||
@ -47,6 +48,10 @@ extension PureXML.Schema.XSDParser {
|
||||
errors.append("a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint")
|
||||
return
|
||||
}
|
||||
if hasDefaultWithNonOptionalUse(node) {
|
||||
errors.append("an 'attribute' with a 'default' value constraint must have use='optional'")
|
||||
return
|
||||
}
|
||||
guard let typeName = PureXML.Schema.XSDNode.attribute(node, "type") else { return }
|
||||
let fixed = PureXML.Schema.XSDNode.attribute(node, "fixed")
|
||||
guard let value = fixed ?? PureXML.Schema.XSDNode.attribute(node, "default") else { return }
|
||||
@ -69,9 +74,14 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the unprefixed (no-namespace) attribute named `local`, or nil.
|
||||
private static func unprefixedValue(_ node: XSDTree, _ local: String) -> String? {
|
||||
node.attributes.first { $0.name.prefix == nil && $0.name.localName == local }?.value
|
||||
}
|
||||
|
||||
/// Whether `node` carries the unprefixed (no-namespace) attribute named `local`.
|
||||
private static func hasUnprefixedAttribute(_ node: XSDTree, _ local: String) -> Bool {
|
||||
node.attributes.contains { $0.name.prefix == nil && $0.name.localName == local }
|
||||
unprefixedValue(node, local) != nil
|
||||
}
|
||||
|
||||
/// Whether an XSD-namespace declaration carries both a `default` and a `fixed`
|
||||
@ -82,6 +92,19 @@ extension PureXML.Schema.XSDParser {
|
||||
&& hasUnprefixedAttribute(node, "fixed")
|
||||
}
|
||||
|
||||
/// Whether an XSD-namespace `attribute` use carries a `default` together with a
|
||||
/// `use` other than `optional`, which `src-attribute.2` forbids: a default value
|
||||
/// makes the attribute optional, so `use="required"` or `use="prohibited"` is a
|
||||
/// contradiction.
|
||||
private static func hasDefaultWithNonOptionalUse(_ node: XSDTree) -> Bool {
|
||||
guard node.name?.namespaceURI == xsdNamespace,
|
||||
PureXML.Schema.XSDNode.localName(node) == "attribute",
|
||||
hasUnprefixedAttribute(node, "default"),
|
||||
let use = unprefixedValue(node, "use")
|
||||
else { return false }
|
||||
return use == "required" || use == "prohibited"
|
||||
}
|
||||
|
||||
private static let facetNames: Set<String> = [
|
||||
"minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits",
|
||||
"fractionDigits", "length", "minLength", "maxLength", "enumeration", "whiteSpace", "pattern",
|
||||
|
||||
40
Tests/SchemaAttributeDefaultUseTests.swift
Normal file
40
Tests/SchemaAttributeDefaultUseTests.swift
Normal file
@ -0,0 +1,40 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XSD attribute default requires use=optional (src-attribute.2)")
|
||||
struct SchemaAttributeDefaultUseTests {
|
||||
private let xsd = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
|
||||
|
||||
private func compiles(_ body: String) -> Bool {
|
||||
(try? PureXML.Schema.Document("<xs:schema \(xsd)>\(body)</xs:schema>")) != nil
|
||||
}
|
||||
|
||||
private func attribute(_ attrs: String) -> String {
|
||||
"<xs:element name=\"e\"><xs:complexType>"
|
||||
+ "<xs:attribute name=\"a\" type=\"xs:string\" \(attrs)/>"
|
||||
+ "</xs:complexType></xs:element>"
|
||||
}
|
||||
|
||||
@Test("A default with use=required is rejected")
|
||||
func test_defaultRequiredRejected() {
|
||||
#expect(!compiles(attribute("default=\"x\" use=\"required\"")))
|
||||
}
|
||||
|
||||
@Test("A default with use=prohibited is rejected")
|
||||
func test_defaultProhibitedRejected() {
|
||||
#expect(!compiles(attribute("default=\"x\" use=\"prohibited\"")))
|
||||
}
|
||||
|
||||
@Test("A default with use=optional, or with no use, compiles")
|
||||
func test_defaultOptionalAccepted() {
|
||||
#expect(compiles(attribute("default=\"x\" use=\"optional\"")))
|
||||
#expect(compiles(attribute("default=\"x\"")))
|
||||
}
|
||||
|
||||
/// `src-attribute.2` constrains only `default`; a `fixed` with `use="required"`
|
||||
/// is a valid required-with-fixed-value attribute and must compile.
|
||||
@Test("A fixed with use=required compiles (the rule is about default only)")
|
||||
func test_fixedRequiredAccepted() {
|
||||
#expect(compiles(attribute("fixed=\"x\" use=\"required\"")))
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ 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 = 469
|
||||
private let knownSchemaInvalidAccepted = 462
|
||||
private let knownInstanceValidRejected = 180
|
||||
private let knownInstanceInvalidAccepted = 160
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ 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 -> 469** | 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 -> 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). |
|
||||
| valid-instances-rejected | **233 -> 180** | (instance side; #164's element-ref namespace fix is shared with instance validation, -34) |
|
||||
| invalid-instances-accepted | 160 | |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user