fix(schema): enforce identity-constraint field cardinality (#147)
An identity-constraint field must evaluate to a node-set with at most one member (cvc-identity-constraint.3). The validator built a value tuple per target without checking how many nodes each field selected, so an instance whose unique/key/keyref field matched several nodes was accepted. fieldCardinalityError now rejects a target whose field selects more than one node, in both the key/unique and keyref paths. XSTS invalid-instances-accepted 4 → fewer (147 to 143), no other bucket moved.
This commit is contained in:
parent
4df9eda52e
commit
9769f022dd
@ -22,7 +22,9 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS valid-instances-rejected (#146): **173 → 3** on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/3/147). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing `\p{Is…}` block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match `\D` (`XSDCategory`, `Unicode32Assigned`). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their own `fixed` values and an element matching a `skip` wildcard is not validated against its global declaration. Schema documents that form an import chain are compiled as a union rather than merged as separately compiled documents, so cross-document substitution groups resolve (#161, `Document.init(composing:)`). Plus attribute-group `anyAttribute` inheritance, extension wildcard union, namespace-qualified type tables for multi-document `xsi:schemaLocation`, per-container `elementFormDefault`, strict/lax attribute wildcard validation, built-in type resolution, and `xs:redefine` simpleType overrides.
|
||||
- XSTS invalid-instances-accepted (#147): identity-constraint fields now enforce cardinality (a `field` must select at most one node; cvc-identity-constraint.3), so an instance whose `unique`/`key`/`keyref` field matches several nodes is rejected rather than accepted. Caught four cases (idF005, idG005, and kin): invalid-instances-accepted 147 → 143, no other bucket moved.
|
||||
|
||||
- XSTS valid-instances-rejected (#146): **173 → 3** on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/3/143). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing `\p{Is…}` block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match `\D` (`XSDCategory`, `Unicode32Assigned`). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their own `fixed` values and an element matching a `skip` wildcard is not validated against its global declaration. Schema documents that form an import chain are compiled as a union rather than merged as separately compiled documents, so cross-document substitution groups resolve (#161, `Document.init(composing:)`). Plus attribute-group `anyAttribute` inheritance, extension wildcard union, namespace-qualified type tables for multi-document `xsi:schemaLocation`, per-container `elementFormDefault`, strict/lax attribute wildcard validation, built-in type resolution, and `xs:redefine` simpleType overrides.
|
||||
|
||||
- Enforce element, attribute, attributeGroup, and group child-content shape (schema-for-schemas cardinality and order): at most one inline type per element/attribute, inline type before identity constraints, at most one compositor per group, at most one `anyAttribute` per attributeGroup with attribute references preceding it (`SchemaStructureHelpers.swift`).
|
||||
|
||||
|
||||
@ -204,8 +204,12 @@ extension PureXML.Schema {
|
||||
var tuples: [[FieldValue?]] = []
|
||||
var seen: [[FieldValue?]] = []
|
||||
for target in select(constraint.selector, at: node) {
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
let targetPath = path + steps(from: node, to: target)
|
||||
if let error = fieldCardinalityError(constraint, at: target, path: targetPath) {
|
||||
issues.append(error)
|
||||
continue
|
||||
}
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
|
||||
issues.append(.init(reason: "key '\(constraint.name)': a field is missing", at: targetPath + fieldStep(constraint.fields, at: target)))
|
||||
continue
|
||||
@ -231,15 +235,37 @@ extension PureXML.Schema {
|
||||
guard case let .keyref(refer) = constraint.kind else { return }
|
||||
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
|
||||
for target in select(constraint.selector, at: node) {
|
||||
let targetPath = path + steps(from: node, to: target)
|
||||
if let error = fieldCardinalityError(constraint, at: target, path: targetPath) {
|
||||
issues.append(error)
|
||||
continue
|
||||
}
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
if tuple.contains(where: { $0 == nil }) { continue }
|
||||
if !keyTuples.contains(where: { $0 == tuple }) {
|
||||
let targetPath = path + steps(from: node, to: target)
|
||||
issues.append(.init(reason: "keyref '\(constraint.name)': no matching key '\(refer)'", at: targetPath + fieldStep(constraint.fields, at: target)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error when a field of `constraint` selects more than one node at
|
||||
/// `target`. A field must evaluate to a node-set with at most one member
|
||||
/// (Identity-constraint Definition; cvc-identity-constraint.3); selecting
|
||||
/// several is a violation, not a multi-valued key.
|
||||
private func fieldCardinalityError(
|
||||
_ constraint: IdentityConstraint,
|
||||
at target: PureXML.Model.TreeNode,
|
||||
path targetPath: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
for field in constraint.fields where select(field, at: target).count > 1 {
|
||||
return .init(
|
||||
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node",
|
||||
at: targetPath + fieldStep(constraint.fields, at: target),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: XPath helpers
|
||||
|
||||
private func select(_ xpath: String, at node: PureXML.Model.TreeNode) -> [PureXML.Model.TreeNode] {
|
||||
|
||||
@ -52,10 +52,15 @@ struct XSTSSuiteTests {
|
||||
/// substitution group: instances rejected 4 → 3, no other bucket moved. The
|
||||
/// remaining three are the Ethiopic-digit `\d` cases (reS17, reS38, reZ004v),
|
||||
/// which contradict `\d` = `\p{Nd}` and are tracked as suspect tests.
|
||||
///
|
||||
/// Enforcing identity-constraint field cardinality (a field must select at most
|
||||
/// one node; cvc-identity-constraint.3) then caught four invalid instances that
|
||||
/// were accepted (idF005, idG005, and kin): invalid accepted 147 → 143, no
|
||||
/// other bucket moved.
|
||||
private let knownSchemaValidRejected = 1
|
||||
private let knownSchemaInvalidAccepted = 262
|
||||
private let knownInstanceValidRejected = 3
|
||||
private let knownInstanceInvalidAccepted = 147
|
||||
private let knownInstanceInvalidAccepted = 143
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user