fix(schema): detect UPA overlap of ##other wildcards
Unique Particle Attribution missed overlapping xs:any wildcards involving ##other: wildcardsOverlap returned false for any pair touching .other, so two competing wildcards whose namespaces actually intersect were not flagged as non-deterministic and the schema was wrongly accepted. Compute the overlap correctly: ##other against ##other always overlaps (both admit every namespace but their target and the absent one, infinite sets that always intersect), and ##other against an enumerated set overlaps iff the set names a namespace that is neither absent (##local) nor the target. The new predicate matches Wildcard.admits exactly, so disjoint cases (##other vs ##local, or vs the target namespace) stay deterministic and no valid schema is newly rejected. Verified against the full W3C XSTS 2006-11-06 suite: invalid-schemas-accepted 35 -> 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected held at 0 (no other bucket moved). Adds UPA unit tests for both rejection shapes and a ##other-vs-##local determinism guard; updates the pinned XSTS count 35 -> 31.
This commit is contained in:
parent
97c07e91f4
commit
9a2cb7eab3
@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unique Particle Attribution now detects overlapping `xs:any` wildcards involving `##other`. `wildcardsOverlap` returned false for any pair involving `##other`, so two competing wildcards whose namespaces actually intersect, two `##other` constraints (which always share infinitely many namespaces) or `##other` against an enumerated set naming a non-target, non-absent namespace, were not flagged as non-deterministic, and the schema was wrongly accepted. The overlap is now computed correctly: `##other`∩`##other` always overlaps, and `##other`∩enumerated overlaps iff the set names a namespace that is neither absent (`##local`) nor the target. Disjoint cases (`##other` against `##local` or against the target namespace) remain deterministic, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 35 → 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected held at 0.
|
||||
- The content-model matcher no longer blows up on a nested model group with a huge but
|
||||
finite `maxOccurs` (#192, partial). The counted-automaton active set is bounded by the
|
||||
product of the per-counter saturation caps; a finite `maximum` the input cannot reach
|
||||
|
||||
@ -290,14 +290,30 @@ public extension PureXML.Schema {
|
||||
private static func wildcardsOverlap(_ lhs: Wildcard, _ rhs: Wildcard) -> Bool {
|
||||
switch (lhs.namespace, rhs.namespace) {
|
||||
case (.any, _), (_, .any):
|
||||
// `##any` admits every name, so it shares names with any other constraint.
|
||||
true
|
||||
case (.other, .other):
|
||||
// Two `##other` constraints each admit every namespace but their own
|
||||
// target (and the absent namespace); those infinite sets always
|
||||
// intersect, so they overlap (XSTS wildI* non-deterministic choices).
|
||||
true
|
||||
case let (.enumerated(lhsSet), .enumerated(rhsSet)):
|
||||
!lhsSet.isDisjoint(with: rhsSet)
|
||||
default:
|
||||
false
|
||||
case let (.other, .enumerated(set)):
|
||||
otherOverlapsEnumerated(set, target: lhs.targetNamespace)
|
||||
case let (.enumerated(set), .other):
|
||||
otherOverlapsEnumerated(set, target: rhs.targetNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `##other` (any namespace but the target and the absent namespace)
|
||||
/// shares a name with an enumerated set: it does iff the set names some
|
||||
/// namespace that is neither absent (the empty string, `##local`) nor the
|
||||
/// target namespace `##other` excludes.
|
||||
private static func otherOverlapsEnumerated(_ set: Set<String>, target: String?) -> Bool {
|
||||
set.contains { !$0.isEmpty && $0 != (target ?? "") }
|
||||
}
|
||||
|
||||
private static func describe(_ label: TermLabel) -> String {
|
||||
switch label {
|
||||
case let .name(name): name.localName
|
||||
|
||||
@ -46,6 +46,25 @@ struct ContentModelDeterminismTests {
|
||||
#expect(rejects(complexType(#"<xs:sequence><xs:element name="a" maxOccurs="unbounded"/><xs:element name="a"/></xs:sequence>"#)))
|
||||
}
|
||||
|
||||
@Test("a choice of ##other and an overlapping namespace wildcard is ambiguous")
|
||||
func test_choiceOfOtherAndOverlappingWildcard() {
|
||||
// `##other` admits namespace A (A is neither absent nor the target), and so
|
||||
// does `namespace="A"`, so an A-namespaced element matches both (XSTS wildI009).
|
||||
#expect(rejects(complexType("<xs:choice><xs:any namespace='##other'/><xs:any namespace='A'/></xs:choice>")))
|
||||
}
|
||||
|
||||
@Test("a choice of two ##other wildcards is ambiguous")
|
||||
func test_choiceOfTwoOtherWildcards() {
|
||||
#expect(rejects(complexType("<xs:choice><xs:any namespace='##other'/><xs:any namespace='##other'/></xs:choice>")))
|
||||
}
|
||||
|
||||
@Test("a choice of ##other and ##local is deterministic and compiles")
|
||||
func test_choiceOfOtherAndLocalCompiles() {
|
||||
// `##other` never admits the absent namespace and `##local` admits only it,
|
||||
// so they are disjoint: the model is deterministic and must not be rejected.
|
||||
#expect(!rejects(complexType("<xs:choice><xs:any namespace='##other'/><xs:any namespace='##local'/></xs:choice>")))
|
||||
}
|
||||
|
||||
@Test("two same-name members of an all group are ambiguous")
|
||||
func test_allWithSameName() {
|
||||
#expect(rejects(complexType(#"<xs:all><xs:element name="a"/><xs:element name="a"/></xs:all>"#)))
|
||||
|
||||
@ -143,12 +143,12 @@ struct XSTSSuiteTests {
|
||||
/// full effective-total-range Particle-Valid-Restriction (tracked).
|
||||
/// Further rules, each with valid-schemas-rejected held at 0 (see CHANGELOG and
|
||||
/// docs/xsts-deviations.md), drove invalid-schemas-accepted 130 -> 45 across
|
||||
/// model-group occurrence, identity-constraint content, derivation base kind,
|
||||
/// attribute/simple-type rules, composition, simpleContent inline-type checks,
|
||||
/// complexContent mixedness, and substitution-group anySimpleType checks.
|
||||
/// model-group occurrence, identity-constraint content, derivation base kind, attribute/simple-type,
|
||||
/// composition, simpleContent inline-type, complexContent mixedness, and substitution-group anySimpleType checks.
|
||||
/// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35.
|
||||
/// UPA wildcard-overlap (`##other` was treated as overlapping nothing) 35->31 (wildI009/I013/I014 + one more), no other bucket moved.
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 35
|
||||
private let knownSchemaInvalidAccepted = 31
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in
|
||||
|
||||
Loading…
Reference in New Issue
Block a user