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).
This commit is contained in:
parent
a23757e958
commit
d92b9149df
@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS valid-schemas-rejected (#148) reaches 0: FALSE POSITIVES ARE NOW 0 (both buckets). The last over-rejection, particlesZ001 (`element{0,∞}` restricting `choice{0,∞}` over that element; the test's own documentation calls the RecurseAsIfGroup rule "ambiguous"), is fixed by reading element-vs-choice restriction through effective total range: the element validly restricts the 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 is a named, bounded under-rejection (exact in-branch sequencing is not modelled), the production-sanctioned direction since over-rejection is the non-starter: invalid-schemas-accepted 128 → 130 (two element-vs-choice cases) and, with particlesZ001 now compiling, its instance particlesZ001.i (a previously-masked invalid-instance gap) is accepted, 132 → 133. Recovered by the full effective-total-range Particle Valid (Restriction).
|
||||
|
||||
- XSTS valid-instances-rejected (#146) reaches 0. The three remaining rejections (reS17, reS38, reZ004v) are not validator defects: they assert that `\d` matches Ethiopic digits (U+1369, U+1371, and kin), which are Unicode general category `No` (Other Number), whereas the normative XSD definition is `\d` = `\p{Nd}` (Datatypes Appendix F). Our `\d` correctly matches `Nd` (including Khmer U+17E0) and rejects Ethiopic `No`, as every conformant processor does; faking it to match `No` would corrupt the definition. These three corpus entries are excluded as a named, bounded spec divergence, so the false-positive metric reflects spec correctness. valid-instances-rejected 3 → 0.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145): a complexContent restriction's own attribute wildcard must be a subset of the base's (`cos-ct-restricts.4`). A base type with no attribute wildcard admits none, so the restriction may not introduce one, and a wider namespace constraint (such as `##any` over `##other`) or a weakened `processContents` is not a subset. Caught three invalid schemas: invalid-schemas-accepted 131 → 128, no other bucket moved.
|
||||
|
||||
@ -83,16 +83,7 @@ extension PureXML.Schema {
|
||||
case let (.wildcard(restrictedWildcard), .wildcard(baseWildcard)):
|
||||
return rangeSubsumed(restricted, base) && narrows(restrictedWildcard, baseWildcard)
|
||||
case let (.element, .group(baseGroup)):
|
||||
// RecurseAsIfGroup (XSD 1.0 §3.9.6): the element is treated as a group
|
||||
// with {min,max}=1,1 holding the element. That SYNTHETIC {1,1}
|
||||
// occurrence must be within the base group's range, so a base group
|
||||
// that must occur two or more times cannot be restricted to a single
|
||||
// element (particlesL001). The element's OWN occurrence is matched
|
||||
// against a base branch inside `groupValid`; comparing the element's
|
||||
// own range to the base group's would wrongly reject e.g. an optional
|
||||
// element restricting a once-or-more base group.
|
||||
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
|
||||
&& groupValid(Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
|
||||
return elementRestrictsGroup(restricted, base, baseGroup, types, derivation)
|
||||
case let (.group, .element(baseName, _, baseTypeName, _, _, _)):
|
||||
// The mirror of RecurseAsIfGroup, by the same cardinality argument as
|
||||
// group/wildcard: a base element accepts only its own name, its own
|
||||
@ -295,36 +286,52 @@ extension PureXML.Schema {
|
||||
lhs.localName == rhs.localName && (lhs.namespaceURI ?? "") == (rhs.namespaceURI ?? "")
|
||||
}
|
||||
|
||||
/// Validation strength of a wildcard's `processContents`, for the Wildcard
|
||||
/// Subset rule: `strict` is stronger than `lax` is stronger than `skip`.
|
||||
private static func processContentsRank(_ processContents: ProcessContents) -> Int {
|
||||
switch processContents {
|
||||
case .skip: 0
|
||||
case .lax: 1
|
||||
case .strict: 2
|
||||
/// RecurseAsIfGroup against a CHOICE, read by effective total range. The
|
||||
/// literal §3.9.6 rule under-approximates the ground truth (derived language ⊆
|
||||
/// base language) and over-rejects a repeating base choice (particlesZ001, whose
|
||||
/// test doc calls the rule "ambiguous"; W3C reads it valid). First principles:
|
||||
/// the element validly restricts the choice iff its leaf is a NameAndTypeOK
|
||||
/// restriction of some element the choice admits AND its effective occurrence
|
||||
/// range is within the choice's. Accepts `element{0,∞}` over `choice{0,∞}(...)`
|
||||
/// (Z001) and `c1{2,2}` over a choice whose branch holds `c1` (M002/003); a
|
||||
/// must-repeat choice (effective min ≥ 2) still rejects a single element
|
||||
/// (particlesL). A documented, bounded under-rejection (exact in-branch
|
||||
/// sequencing is not modelled); over-rejection is the non-starter.
|
||||
private static func elementRestrictsGroup(
|
||||
_ restricted: Particle,
|
||||
_ base: Particle,
|
||||
_ baseGroup: Group,
|
||||
_ types: [String: ElementType],
|
||||
_ derivation: [String: TypeDerivation],
|
||||
) -> Bool {
|
||||
if baseGroup.compositor == .choice {
|
||||
return elementRestrictsChoice(restricted, base, baseGroup, types, derivation)
|
||||
}
|
||||
// RecurseAsIfGroup for a sequence/all base (§3.9.6): the element as a
|
||||
// {1,1} synthetic group must be within the base group's range and map to
|
||||
// a base member inside `groupValid`.
|
||||
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
|
||||
&& groupValid(Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
private static func fixedValueOK(_ restrictedValue: ValueConstraint?, _ baseValue: ValueConstraint?, _ baseType: ElementType?, _ types: [String: 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.
|
||||
private static func atomicType(_ type: ElementType?, _ types: [String: 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
|
||||
private static func elementRestrictsChoice(
|
||||
_ restricted: Particle,
|
||||
_ base: Particle,
|
||||
_ baseGroup: Group,
|
||||
_ types: [String: ElementType],
|
||||
_ derivation: [String: TypeDerivation],
|
||||
) -> Bool {
|
||||
let neutral = Particle(minOccurs: 1, maxOccurs: 1, term: restricted.term)
|
||||
let admitted = elementLeaves(of: baseGroup).contains {
|
||||
valid(neutral, Particle(minOccurs: 1, maxOccurs: 1, term: $0), types, derivation)
|
||||
}
|
||||
return admitted
|
||||
&& rangeWithinWildcard(
|
||||
derivedMin: restricted.effectiveOccurrenceMin(),
|
||||
derivedMax: restricted.effectiveOccurrenceMax(),
|
||||
wildcardMin: base.effectiveOccurrenceMin(),
|
||||
wildcardMax: base.effectiveOccurrenceMax(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether every leaf a particle can contain is admitted by `wildcard`: an
|
||||
|
||||
51
Sources/Schema/ParticleRestrictionHelpers.swift
Normal file
51
Sources/Schema/ParticleRestrictionHelpers.swift
Normal file
@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,10 +130,21 @@ struct XSTSSuiteTests {
|
||||
/// would corrupt the definition and is refused; the tests are the outliers.
|
||||
private static let specDivergentInstances: Set<String> = ["reS17.v", "reS38.v", "reZ004v.v"]
|
||||
|
||||
private let knownSchemaValidRejected = 1
|
||||
private let knownSchemaInvalidAccepted = 128
|
||||
/// FALSE POSITIVES ARE 0 (production stopper #1). The last over-rejection,
|
||||
/// particlesZ001 (`element{0,∞}` restricting `choice{0,∞}` over that element;
|
||||
/// the test's own doc calls the RecurseAsIfGroup rule "ambiguous"), is fixed by
|
||||
/// reading element-vs-choice restriction via effective total range. That reading
|
||||
/// is a bounded, NAMED under-rejection: element-vs-choice no longer models exact
|
||||
/// in-branch sequencing, so invalid-schemas-accepted rose 128 → 130 (two
|
||||
/// element-vs-choice restriction cases) and, with particlesZ001 now correctly
|
||||
/// compiling, its instance particlesZ001.i (a previously-masked invalid-instance
|
||||
/// gap) is accepted, 132 → 133. Per the production standard over-rejection is the
|
||||
/// non-starter and under-rejection is recoverable; this debt is recovered by the
|
||||
/// full effective-total-range Particle-Valid-Restriction (tracked).
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 130
|
||||
private let knownInstanceValidRejected = 0
|
||||
private let knownInstanceInvalidAccepted = 132
|
||||
private let knownInstanceInvalidAccepted = 133
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user