The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass.
185 lines
10 KiB
Swift
185 lines
10 KiB
Swift
private typealias AttrRestrictNode = PureXML.Schema.XSDNode
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
/// XSD 1.0 Derivation Valid (Restriction, Complex) for attribute uses
|
|
/// (`cos-ct-derived-ok` / `derivation-ok-restriction.2`): when a complex type
|
|
/// restricts a base, an attribute the restriction redeclares may not relax the
|
|
/// base's corresponding use. A base `required` attribute must stay required: it
|
|
/// may not become optional or prohibited. Such schemas were accepted.
|
|
///
|
|
/// Only attributes the restriction explicitly declares that also exist in the
|
|
/// base are checked; an attribute the restriction omits is inherited unchanged
|
|
/// (valid), and an attribute with no base counterpart is a separate rule, left
|
|
/// alone. Checked for a self-contained schema (no `import`/`include`/`redefine`),
|
|
/// where the base resolves locally and attribute names are unambiguous; the base
|
|
/// reference must resolve to this schema's own target namespace.
|
|
///
|
|
/// Disclosed under-rejection: the matching fixed-value clause (a base attribute
|
|
/// fixed to a value must keep that value) is not enforced here. It requires
|
|
/// comparing the two fixed values in the attribute type's value space, not
|
|
/// lexically (e.g. a list type's `"1 2 3"` and `"1 2 3"` are the same value),
|
|
/// which a string comparison gets wrong; that clause is left for a later change.
|
|
static func attributeRestrictionFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
// The attribute-use restriction rules apply to a restriction of either
|
|
// content kind: a `simpleContent` restriction (a complex type with simple
|
|
// content plus attributes) narrows its base attributes by the same rules as
|
|
// a `complexContent` one, so both derivation roots are checked here.
|
|
for content in descendants(schema, named: "complexContent") + descendants(schema, named: "simpleContent") {
|
|
guard let restriction = AttrRestrictNode.firstChild(content, named: "restriction"),
|
|
let base = AttrRestrictNode.attribute(restriction, "base"),
|
|
AttrRestrictNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
|
case let .complex(complex)? = types[AttrRestrictNode.stripPrefix(base)]
|
|
else { continue }
|
|
// An attribute use resolves to a model value with no source node, so a
|
|
// faithful-restriction violation locates on the restriction performing
|
|
// the illegal narrowing, the node a developer would correct.
|
|
for derived in attributeUses(under: restriction, context) {
|
|
if let message = attributeRestrictionViolation(derived, complex.attributes, complex.attributeWildcard, restriction, context) {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: message, node: restriction))
|
|
}
|
|
}
|
|
// cos-ct-restricts.4: a restriction's own attribute wildcard must be a
|
|
// subset of the base's. A base with no wildcard admits none, so the
|
|
// restriction may not introduce one (ctO005); a wider namespace
|
|
// constraint, such as `##any` over `##other`, is not a subset (ctO007).
|
|
if let derivedWildcard = attributeWildcard(under: restriction, context) {
|
|
if let baseWildcard = complex.attributeWildcard {
|
|
if !PureXML.Schema.ParticleRestriction.narrows(derivedWildcard, baseWildcard) {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "the restriction's attribute wildcard is not a subset of the base type's attribute wildcard",
|
|
node: restriction,
|
|
))
|
|
}
|
|
} else {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a restriction may not introduce an attribute wildcard that the base type does not have",
|
|
node: restriction,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// The way `derived` (an attribute the restriction redeclares) illegally relaxes
|
|
/// its base counterpart, or nil when it is a valid restriction of it (or has no
|
|
/// base counterpart). A base `required` attribute may not be made optional or
|
|
/// prohibited (a prohibited use has `required == false`).
|
|
/// Also checks that fixed attributes keep their fixed values.
|
|
private static func attributeRestrictionViolation(
|
|
_ derived: PureXML.Schema.AttributeUse,
|
|
_ baseAttributes: [PureXML.Schema.AttributeUse],
|
|
_ baseWildcard: PureXML.Schema.Wildcard?,
|
|
_ restrictionNode: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
) -> String? {
|
|
guard let base = baseAttributes.first(where: { $0.name == derived.name }) else {
|
|
// No matching base attribute use (cos-ct-restricts.3): a `prohibited`
|
|
// declaration removes an attribute (valid); otherwise the base's
|
|
// attribute wildcard must admit it, else the restriction adds an
|
|
// attribute the base never permitted (ctO004: a no-namespace attribute
|
|
// over a `##other` wildcard).
|
|
if isProhibited(derived.name, under: restrictionNode, context) {
|
|
return nil
|
|
}
|
|
if baseWildcard?.admits(derived.name) == true {
|
|
return nil
|
|
}
|
|
return "attribute '\(derived.name.localName)' is not in the base type and is not admitted by its attribute wildcard, so a restriction may not add it"
|
|
}
|
|
if base.required, !derived.required {
|
|
return "attribute '\(derived.name.localName)' is required in the base type and a restriction may not make it optional or prohibited"
|
|
}
|
|
if isProhibited(derived.name, under: restrictionNode, context) {
|
|
return nil
|
|
}
|
|
if !isSimpleTypeRestrictionOK(derived: derived.type, base: base.type) {
|
|
return "attribute '\(derived.name.localName)' has type '\(derived.type.base.rawValue)' which is not a valid restriction of base type '\(base.type.base.rawValue)'"
|
|
}
|
|
if let baseFixed = base.valueConstraint?.fixedValue {
|
|
guard let derivedConstraint = derived.valueConstraint,
|
|
let derivedFixed = derivedConstraint.fixedValue
|
|
else {
|
|
return "attribute '\(derived.name.localName)' is fixed in the base type and a restriction must also make it fixed"
|
|
}
|
|
if !derived.type.valueMatches(derivedFixed, literal: baseFixed) {
|
|
return "attribute '\(derived.name.localName)' has fixed value '\(derivedFixed)' which does not match base fixed value '\(baseFixed)' in the type's value space"
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func isSimpleTypeRestrictionOK(derived: PureXML.Schema.SimpleType, base: PureXML.Schema.SimpleType) -> Bool {
|
|
if base.isAnySimpleType {
|
|
return true
|
|
}
|
|
if derived.isAnySimpleType {
|
|
return false
|
|
}
|
|
switch (derived.variety, base.variety) {
|
|
case (.atomic, .atomic):
|
|
return derived.base.derives(from: base.base)
|
|
case let (.list(derivedItem), .list(baseItem)):
|
|
return isSimpleTypeRestrictionOK(derived: derivedItem, base: baseItem)
|
|
case (.union, .union):
|
|
guard case let .union(derivedMembers) = derived.variety,
|
|
case let .union(baseMembers) = base.variety
|
|
else { return false }
|
|
return derivedMembers.allSatisfy { derivedMember in
|
|
baseMembers.contains { baseMember in
|
|
isSimpleTypeRestrictionOK(derived: derivedMember, base: baseMember)
|
|
}
|
|
}
|
|
case (_, .union):
|
|
guard case let .union(baseMembers) = base.variety else { return false }
|
|
return baseMembers.contains { baseMember in
|
|
isSimpleTypeRestrictionOK(derived: derived, base: baseMember)
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func isProhibited(_ name: PureXML.Model.QualifiedName, under node: XSDTree, _ context: PureXML.Schema.XSDContext, visited: Set<String> = []) -> Bool {
|
|
for child in AttrRestrictNode.elementChildren(node) {
|
|
switch AttrRestrictNode.localName(child) {
|
|
case "attribute":
|
|
if let attrName = attributeName(child, context), attrName == name {
|
|
if AttrRestrictNode.attribute(child, "use") == "prohibited" {
|
|
return true
|
|
}
|
|
}
|
|
case "attributeGroup":
|
|
guard let ref = AttrRestrictNode.attribute(child, "ref") else { break }
|
|
let refName = AttrRestrictNode.stripPrefix(ref)
|
|
guard !visited.contains(refName), let group = context.attributeGroups[refName] else { break }
|
|
if isProhibited(name, under: group, context, visited: visited.union([refName])) {
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private static func attributeName(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Model.QualifiedName? {
|
|
if let ref = AttrRestrictNode.attribute(node, "ref") {
|
|
let refName = AttrRestrictNode.stripPrefix(ref)
|
|
let namespace = AttrRestrictNode.referenceNamespace(ref, context.namespaceBindings)
|
|
return PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace)
|
|
}
|
|
guard let name = AttrRestrictNode.attribute(node, "name") else { return nil }
|
|
let qualified = AttrRestrictNode.attribute(node, "form") == "qualified"
|
|
|| (AttrRestrictNode.attribute(node, "form") == nil && context.attributeFormQualified)
|
|
return PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil)
|
|
}
|
|
}
|