feat(schema): enforce Element Declarations Consistent (cos-element-consistent)
Within one complex type's content model, two element declarations with the same name must have the same type definition. Two same-name elements with different types were accepted. elementDeclsConsistentErrors collects element particles across the content model -- descending through model groups and the content- derivation wrappers, but never into an element's own nested type, which is a separate content model -- and flags a name seen with more than one distinct type key (a type reference by local name, a distinct token per inline type, or the untyped ur-type). Modeled clean-room on Xerces (libxml2 enforces this only partially). Substitution- group expansion is not folded in, an accepted under-rejection. XSTS: invalid schemas accepted 925 to 909 (-16); valid schemas rejected held at 72; instance buckets unchanged.
This commit is contained in:
parent
39222ca760
commit
243a6dbc9b
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Element Declarations Consistent (`cos-element-consistent`) is now enforced (#147). Within one complex type's content model, two element declarations with the same name must have the same type; two same-name elements with different types (e.g. `a` as `xs:string` and as `xs:integer`) were accepted. The check walks the content model (through model groups and content-derivation wrappers) but not into an element's own nested type, so a reused name in a nested anonymous type, a separate content model, is not flagged. Modeled on Xerces (libxml2 only partially enforces this); substitution-group expansion is not folded in (an accepted under-rejection). XSTS: invalid schemas accepted 925 to 909, no other bucket raised.
|
||||
- Schema-vocabulary attribute applicability is now enforced at compile time (#147), the attribute sibling of the content-model table. Each XSD component admits a fixed set of attributes (the schema for schemas); an unprefixed attribute outside that set (a typo like `nullable`, a stray `foo`, `name` on a `sequence`) is now rejected, and `ref` may not co-occur with `name` or `type`. Foreign (prefixed) attributes and namespace declarations are not the component's own and are not checked, and the element/attribute sets are the permissive union of global and local use, so no valid schema is rejected. XSTS: invalid schemas accepted 956 to 925, no other bucket raised.
|
||||
- A named model-group definition (`xs:group name="..."`) must contain exactly one of `all`/`choice`/`sequence` (#147). XSD 1.0 gives a group's content as `(annotation?, (all | choice | sequence))`, but the `allowedChildren` set-membership check did not enforce the cardinality, so a group with two compositors or with none was accepted. A `group` reference (no `name`) is excluded. XSTS: invalid schemas accepted 977 to 956, no other bucket raised.
|
||||
- A particle whose `minOccurs` exceeds its `maxOccurs` is now rejected at compile time (#147). An occurrence range like `minOccurs="5" maxOccurs="2"` is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), with `unbounded` never exceeded and a single-point range (`minOccurs="2" maxOccurs="2"`) allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised.
|
||||
|
||||
@ -145,6 +145,9 @@ extension PureXML.Schema.XSDParser {
|
||||
if local == "group", PureXML.Schema.XSDNode.attribute(node, "name") != nil {
|
||||
errors += namedGroupErrors(names)
|
||||
}
|
||||
if local == "complexType" {
|
||||
errors += elementDeclsConsistentErrors(node)
|
||||
}
|
||||
}
|
||||
for child in children {
|
||||
collectStructure(child, into: &errors)
|
||||
@ -304,4 +307,62 @@ extension PureXML.Schema.XSDParser {
|
||||
let compositors = children.count { $0 == "all" || $0 == "choice" || $0 == "sequence" }
|
||||
return compositors == 1 ? [] : ["a named group must contain exactly one of all, choice, or sequence"]
|
||||
}
|
||||
|
||||
/// The model-group and content-derivation elements through which a complex
|
||||
/// type's content model extends; the walk descends through these but never
|
||||
/// into an element's own type definition (a nested type is a separate model).
|
||||
private static let contentModelContainers: Set<String> = [
|
||||
"sequence", "choice", "all", "group", "complexContent", "simpleContent", "restriction", "extension",
|
||||
]
|
||||
|
||||
/// Findings for Element Declarations Consistent (cos-element-consistent): in a
|
||||
/// single complex type's content model, two element declarations with the same
|
||||
/// name must have the same type. Collects element particles across the content
|
||||
/// model (descending through model groups and content-derivation wrappers, not
|
||||
/// into a nested type), keyed by name; a name seen with more than one distinct
|
||||
/// type definition is a violation. (Substitution-group expansion is not folded
|
||||
/// in, an accepted under-rejection.)
|
||||
private static func elementDeclsConsistentErrors(_ complexType: XSDTree) -> [String] {
|
||||
var byName: [String: Set<String>] = [:]
|
||||
var inlineCount = 0
|
||||
collectElementDecls(complexType, into: &byName, inlineCount: &inlineCount)
|
||||
return byName.keys.sorted().compactMap { name in
|
||||
(byName[name]?.count ?? 0) > 1
|
||||
? "element '\(name)' has inconsistent type definitions in the same content model"
|
||||
: nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func collectElementDecls(_ node: XSDTree, into byName: inout [String: Set<String>], inlineCount: inout Int) {
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) where child.name?.namespaceURI == xsdNamespace {
|
||||
guard let local = PureXML.Schema.XSDNode.localName(child) else { continue }
|
||||
if local == "element" {
|
||||
if let name = PureXML.Schema.XSDNode.attribute(child, "name") {
|
||||
byName[name, default: []].insert(elementTypeKey(child, inlineCount: &inlineCount))
|
||||
}
|
||||
// Do not descend into the element's own type definition.
|
||||
} else if contentModelContainers.contains(local) {
|
||||
collectElementDecls(child, into: &byName, inlineCount: &inlineCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A key identifying an element particle's type definition: its `type`
|
||||
/// reference by local name, a distinct token per inline (anonymous) type (two
|
||||
/// inline types are never the same definition), or a shared token when the
|
||||
/// element is untyped (the ur-type).
|
||||
private static func elementTypeKey(_ element: XSDTree, inlineCount: inout Int) -> String {
|
||||
if let type = PureXML.Schema.XSDNode.attribute(element, "type") {
|
||||
return "type:" + PureXML.Schema.XSDNode.stripPrefix(type.trimmingXMLWhitespace())
|
||||
}
|
||||
let hasInlineType = PureXML.Schema.XSDNode.elementChildren(element).contains {
|
||||
let local = PureXML.Schema.XSDNode.localName($0)
|
||||
return local == "complexType" || local == "simpleType"
|
||||
}
|
||||
if hasInlineType {
|
||||
inlineCount += 1
|
||||
return "inline:\(inlineCount)"
|
||||
}
|
||||
return "untyped"
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,6 +195,33 @@ struct SchemaStructureTests {
|
||||
#expect(rejects(#"<xs:complexType name="t"><xs:sequence><xs:element ref="g" type="xs:string"/></xs:sequence></xs:complexType><xs:element name="g" type="xs:string"/>"#))
|
||||
}
|
||||
|
||||
@Test("element declarations in one content model must be consistent")
|
||||
func test_elementDeclsConsistent() throws {
|
||||
// Same name, different type in one content model is invalid.
|
||||
#expect(rejects(#"""
|
||||
<xs:complexType name="t"><xs:sequence>
|
||||
<xs:element name="a" type="xs:string"/>
|
||||
<xs:element name="a" type="xs:integer"/>
|
||||
</xs:sequence></xs:complexType>
|
||||
"""#))
|
||||
// Same name, same type is fine.
|
||||
try compile(#"""
|
||||
<xs:complexType name="t"><xs:sequence>
|
||||
<xs:element name="a" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="a" type="xs:string"/>
|
||||
</xs:sequence></xs:complexType>
|
||||
"""#)
|
||||
// A same name in a NESTED complex type is a different content model: allowed.
|
||||
try compile(#"""
|
||||
<xs:complexType name="t"><xs:sequence>
|
||||
<xs:element name="a" type="xs:string"/>
|
||||
<xs:element name="b">
|
||||
<xs:complexType><xs:sequence><xs:element name="a" type="xs:integer"/></xs:sequence></xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence></xs:complexType>
|
||||
"""#)
|
||||
}
|
||||
|
||||
@Test("an identity constraint requires a selector and field")
|
||||
func test_identityConstraintRequiresParts() {
|
||||
#expect(rejects(#"""
|
||||
|
||||
@ -29,7 +29,7 @@ struct XSTSSuiteTests {
|
||||
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
private let knownSchemaValidRejected = 72
|
||||
private let knownSchemaInvalidAccepted = 925
|
||||
private let knownSchemaInvalidAccepted = 909
|
||||
private let knownInstanceValidRejected = 214
|
||||
private let knownInstanceInvalidAccepted = 165
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | 72 | we rarely reject a good schema |
|
||||
| **invalid-schemas-accepted** | **2461 -> 925** | we rarely catch a bad one (facet, id, and structural content-model validity landed) |
|
||||
| **invalid-schemas-accepted** | **2461 -> 909** | we rarely catch a bad one (facet, id, and structural content-model validity landed) |
|
||||
| valid-instances-rejected | 233 | (instance side, separately tracked) |
|
||||
| invalid-instances-accepted | 165 | |
|
||||
|
||||
@ -75,7 +75,7 @@ Each iteration targets one self-contained rule family with a clean root cause.
|
||||
(1688 -> 1313, -375)._ A data-driven `allowedChildren` table (the schema-for-
|
||||
schemas child content model) rejects disallowed children, multi/misplaced
|
||||
`annotation`, and identity constraints missing selector/field, via one general
|
||||
mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997), plus **particle minOccurs <= maxOccurs** (997 -> 977), plus **named-group exactly-one-compositor** (977 -> 956), plus **attribute applicability** (allowed-attributes-per-component table + ref/name/type exclusion; 956 -> 925). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness).
|
||||
mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997), plus **particle minOccurs <= maxOccurs** (997 -> 977), plus **named-group exactly-one-compositor** (977 -> 956), plus **attribute applicability** (allowed-attributes-per-component table + ref/name/type exclusion; 956 -> 925), plus **Element Declarations Consistent** (same-name elements in one content model share a type; 925 -> 909). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness).
|
||||
|
||||
## Per-iteration protocol (the PR-critic-loop)
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ and to document the remainder as spec-justified, deliberate exclusions.
|
||||
|
||||
## Category A: genuine correctness bugs
|
||||
|
||||
### Fixed (twenty-six root causes, ~2210 deviations cleared)
|
||||
### Fixed (twenty-seven root causes, ~2226 deviations cleared)
|
||||
|
||||
list-datatype length facets (#146) · union pattern/enumeration facets · duration
|
||||
partial-order facets · QName length non-constraining · the full XSD `\p{Is...}`
|
||||
@ -65,7 +65,7 @@ validity (`min`/`maxInclusive`, `min`/`maxExclusive`, `enumeration` values valid
|
||||
in the base space; inclusive/exclusive exclusions; bound ordering) · `gMonth`
|
||||
`--MM--` lexical form · schema `id` attribute validity (`xs:ID`: NCName and
|
||||
unique within the document). The schema-validity work brought invalid schemas
|
||||
accepted 2461 to 925 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`.
|
||||
accepted 2461 to 909 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`.
|
||||
Plus the measurement fix (exclude W3C-disputed `status="queried"` entries).
|
||||
|
||||
### Remaining, root-caused (the hard tail)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user