PureXML/Sources/Schema/ParticleRestrictionHelpers.swift
Mihaela Mihaljevic d92b9149df fix(schema): eliminate the last false positive (particlesZ001); false-positives = 0 (#148)
Read element-vs-choice Particle Valid (Restriction) through effective total
range: an element validly restricts a base choice when its leaf is a
NameAndTypeOK restriction of an element the choice admits and its effective
occurrence range is within the choice's. This fixes particlesZ001 (the
RecurseAsIfGroup rule its own test calls ambiguous; W3C reads it valid) while
keeping particlesM002/003 valid and particlesL rejected.

Named, bounded under-rejection (exact in-branch sequencing unmodelled), the
production-sanctioned direction: invalid-schemas-accepted 128->130,
invalid-instances-accepted 132->133 (particlesZ001.i, a masked gap now exposed).

XSTS valid-schemas-rejected 1->0. With valid-instances-rejected already 0,
false positives are now 0 in both buckets (production stopper #1).
2026-06-17 19:56:57 +02:00

52 lines
2.4 KiB
Swift

extension PureXML.Schema.ParticleRestriction {
/// Every element-declaration term reachable as a leaf of `group` (recursing
/// into nested groups; wildcards are not elements). Used to test whether a
/// restricting element names something the base group admits.
static func elementLeaves(of group: PureXML.Schema.Group) -> [PureXML.Schema.Term] {
group.particles.flatMap { particle -> [PureXML.Schema.Term] in
switch particle.term {
case .element: [particle.term]
case let .group(inner): elementLeaves(of: inner)
case .wildcard: []
}
}
}
/// Validation strength of a wildcard's `processContents`, for the Wildcard
/// Subset rule: `strict` is stronger than `lax` is stronger than `skip`.
static func processContentsRank(_ processContents: PureXML.Schema.ProcessContents) -> Int {
switch processContents {
case .skip: 0
case .lax: 1
case .strict: 2
}
}
/// NameAndTypeOK value-constraint clause: when the base element is `fixed`, the
/// restricting element must also be `fixed` to the same value (compared in the
/// base type's value space, so `1` and `01` match). A base with no fixed value,
/// or a `default`, imposes no constraint on the restriction.
static func fixedValueOK(
_ restrictedValue: PureXML.Schema.ValueConstraint?,
_ baseValue: PureXML.Schema.ValueConstraint?,
_ baseType: PureXML.Schema.ElementType?,
_ types: [String: PureXML.Schema.ElementType],
) -> Bool {
guard let baseFixed = baseValue?.fixedValue else { return true }
guard let restrictedFixed = restrictedValue?.fixedValue else { return false }
return atomicType(baseType, types)?.valueMatches(restrictedFixed, literal: baseFixed) ?? (restrictedFixed == baseFixed)
}
/// The atomic simple type an element's type resolves to (following a
/// `typeReference` through the types map, bounded against cycles), or nil for a
/// complex, list, or union type.
static func atomicType(_ type: PureXML.Schema.ElementType?, _ types: [String: PureXML.Schema.ElementType], _ depth: Int = 0) -> PureXML.Schema.SimpleType? {
guard depth < 32 else { return nil }
switch type {
case let .simple(simple): return simple
case let .typeReference(key): return atomicType(types[key], types, depth + 1)
default: return nil
}
}
}