cvc-id: when an instance omits an attribute, its default/fixed value applies, so a defaulted or fixed xs:IDREF/xs:IDREFS must still resolve to a matching xs:ID. The reference was recorded only for attributes physically present, so a dangling defaulted IDREF was wrongly accepted. The supplied value is now recorded for document-scoped ID/IDREF checking exactly as a present value would be (shared by the tree and streaming validators, which both call validateAttributes). A required absent attribute is still reported as missing, recordIDs is a no-op for non-ID types, and a defaulted IDREF that does resolve stays valid. Catches idZ012. XSTS invalid-instances-accepted 20 to 19, valid-instances-rejected held at 0.
141 lines
7.1 KiB
Swift
141 lines
7.1 KiB
Swift
extension PureXML.Schema.ComplexValidator {
|
|
func validateAttributes(
|
|
_ element: PureXML.Model.Element,
|
|
_ type: PureXML.Schema.ComplexType,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [PureXML.Validation.ValidationError],
|
|
) {
|
|
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
|
for use in type.attributes where !use.prohibited {
|
|
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
|
if let match {
|
|
if let error = use.type.validate(match.value) {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
|
}
|
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
|
}
|
|
recordIDs(use.type, value: match.value, at: path)
|
|
} else if use.required {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
|
} else if let constraint = use.valueConstraint {
|
|
// An absent attribute takes its `default`/`fixed` value, so a
|
|
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
|
|
// ID (cvc-id): the supplied value participates in document-scoped
|
|
// ID/IDREF checking exactly as a present value would.
|
|
recordIDs(use.type, value: constraint.value, at: path)
|
|
}
|
|
}
|
|
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
|
if idTypedAttributeCount(present, type, on: element) > 1 {
|
|
errors.append(PureXML.Validation.ValidationError(
|
|
reason: "an element may carry at most one attribute of type ID",
|
|
at: path,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// The number of `present` attributes whose effective type is `xs:ID` (an
|
|
/// element may carry at most one; XSD 1.0 §3.4.6, cvc-complex-type). An
|
|
/// attribute's type is its declared use's type when it matches one, otherwise
|
|
/// the global declaration a matching wildcard admits.
|
|
private func idTypedAttributeCount(
|
|
_ present: [PureXML.Model.Attribute],
|
|
_ type: PureXML.Schema.ComplexType,
|
|
on element: PureXML.Model.Element,
|
|
) -> Int {
|
|
present.reduce(into: 0) { count, attribute in
|
|
if isIDTyped(attribute, type, on: element) { count += 1 }
|
|
}
|
|
}
|
|
|
|
/// Whether `attribute`'s effective type is `xs:ID`: the type of the declared
|
|
/// use it matches, or the global declaration a matching wildcard admits.
|
|
private func isIDTyped(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
_ type: PureXML.Schema.ComplexType,
|
|
on element: PureXML.Model.Element,
|
|
) -> Bool {
|
|
if let use = type.attributes.first(where: { !$0.prohibited && Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: element.name) }) {
|
|
return use.type.isID
|
|
}
|
|
guard let wildcard = type.attributeWildcard, wildcard.admits(attribute.name),
|
|
let use = globalAttributeUse(for: attribute.name)
|
|
else { return false }
|
|
return use.type.isID
|
|
}
|
|
|
|
/// Whether an instance attribute satisfies a declared use. Chameleon-included
|
|
/// global attributes may appear unprefixed on an element in the effective TN.
|
|
static func attributeMatches(
|
|
_ instance: PureXML.Model.QualifiedName,
|
|
declared: PureXML.Model.QualifiedName,
|
|
use: PureXML.Schema.AttributeUse,
|
|
on elementName: PureXML.Model.QualifiedName,
|
|
) -> Bool {
|
|
if sameName(instance, declared) { return true }
|
|
guard use.chameleonUnprefixed,
|
|
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
|
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
|
else { return false }
|
|
return instance.localName == declared.localName && declaredNamespace == elementName.namespaceURI
|
|
}
|
|
|
|
private func validateWildcardAttributes(
|
|
_ present: [PureXML.Model.Attribute],
|
|
element: PureXML.Model.Element,
|
|
type: PureXML.Schema.ComplexType,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [PureXML.Validation.ValidationError],
|
|
) {
|
|
// A `use="prohibited"` use is excluded from the effective {attribute uses},
|
|
// so a prohibited attribute is treated as undeclared and left to the
|
|
// attribute wildcard (which may still admit it) or reported as undeclared.
|
|
let declared = type.attributes.filter { !$0.prohibited }
|
|
let elementName = element.name
|
|
let wildcard = type.attributeWildcard
|
|
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
|
|
guard let wildcard, wildcard.admits(attribute.name) else {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
|
continue
|
|
}
|
|
switch wildcard.processContents {
|
|
case .skip:
|
|
continue
|
|
case .lax:
|
|
if let use = globalAttributeUse(for: attribute.name) {
|
|
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
|
}
|
|
case .strict:
|
|
guard let use = globalAttributeUse(for: attribute.name) else {
|
|
errors.append(PureXML.Validation.ValidationError(
|
|
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
|
|
at: path,
|
|
))
|
|
continue
|
|
}
|
|
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func globalAttributeUse(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.AttributeUse? {
|
|
globalAttributes[PureXML.Schema.XSDParser.attributeDeclarationKey(name)]
|
|
}
|
|
|
|
private func validateWildcardAttribute(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
use: PureXML.Schema.AttributeUse,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [PureXML.Validation.ValidationError],
|
|
) {
|
|
if let error = use.type.validate(attribute.value) {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
|
|
}
|
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
|
|
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
|
}
|
|
recordIDs(use.type, value: attribute.value, at: path)
|
|
}
|
|
}
|