diff --git a/CHANGELOG.md b/CHANGELOG.md index aaca59a..0339b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index 5f8e9b3..a1aa9cb 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -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 = [ + "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] = [:] + 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], 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" + } } diff --git a/Tests/SchemaStructureTests.swift b/Tests/SchemaStructureTests.swift index df6c8b5..7233ebe 100644 --- a/Tests/SchemaStructureTests.swift +++ b/Tests/SchemaStructureTests.swift @@ -195,6 +195,33 @@ struct SchemaStructureTests { #expect(rejects(#""#)) } + @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(#""" + + + + + """#)) + // Same name, same type is fine. + try compile(#""" + + + + + """#) + // A same name in a NESTED complex type is a different content model: allowed. + try compile(#""" + + + + + + + """#) + } + @Test("an identity constraint requires a selector and field") func test_identityConstraintRequiresParts() { #expect(rejects(#""" diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 4d4d6d9..fad7de9 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index 7263f4d..dcac00e 100644 --- a/docs/schema-validity-burndown.md +++ b/docs/schema-validity-burndown.md @@ -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) diff --git a/docs/xsts-deviations.md b/docs/xsts-deviations.md index 8f04ec4..358fcfb 100644 --- a/docs/xsts-deviations.md +++ b/docs/xsts-deviations.md @@ -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)