diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3f5b1..aaca59a 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 +- 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. - A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be `simpleContent`, or `complexContent`, or a model group followed by attributes, but never a mix: so at most one of `simpleContent`/`complexContent`, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. The `allowedChildren` set-membership check did not catch these combinations, so a malformed complexType (two `simpleContent`, `simpleContent` plus a `sequence`, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised. diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index bcf21e8..5f8e9b3 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -59,6 +59,77 @@ extension PureXML.Schema.XSDParser { return errors } + /// The unprefixed attributes each XSD component admits (XSD 1.0 schema for + /// schemas). The element/attribute entries are the permissive union across + /// global and local use (so a context-only attribute like `form` or + /// `minOccurs` is never wrongly flagged); `id` is admitted everywhere. An + /// attribute from a foreign namespace (prefixed) is always allowed and is not + /// listed here. + private static let allowedAttributes: [String: Set] = { + let facetAttrs: Set = ["value", "fixed", "id"] + var table: [String: Set] = [ + "schema": ["targetNamespace", "version", "finalDefault", "blockDefault", "attributeFormDefault", "elementFormDefault", "id"], + "element": ["name", "ref", "type", "minOccurs", "maxOccurs", "default", "fixed", "nillable", "abstract", "substitutionGroup", "final", "block", "form", "id"], + "attribute": ["name", "ref", "type", "use", "default", "fixed", "form", "id"], + "complexType": ["name", "abstract", "final", "block", "mixed", "id"], + "simpleType": ["name", "final", "id"], + "complexContent": ["mixed", "id"], + "simpleContent": ["id"], + "restriction": ["base", "id"], + "extension": ["base", "id"], + "group": ["name", "ref", "minOccurs", "maxOccurs", "id"], + "attributeGroup": ["name", "ref", "id"], + "sequence": ["minOccurs", "maxOccurs", "id"], + "choice": ["minOccurs", "maxOccurs", "id"], + "all": ["minOccurs", "maxOccurs", "id"], + "any": ["namespace", "processContents", "minOccurs", "maxOccurs", "id"], + "anyAttribute": ["namespace", "processContents", "id"], + "unique": ["name", "id"], + "key": ["name", "id"], + "keyref": ["name", "refer", "id"], + "selector": ["xpath", "id"], + "field": ["xpath", "id"], + "list": ["itemType", "id"], + "union": ["memberTypes", "id"], + "import": ["namespace", "schemaLocation", "id"], + "include": ["schemaLocation", "id"], + "redefine": ["schemaLocation", "id"], + "notation": ["name", "public", "system", "id"], + "annotation": ["id"], + ] + for facet in ["minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits", "fractionDigits", "length", "minLength", "maxLength", "whiteSpace"] { + table[facet] = facetAttrs + } + for facet in ["enumeration", "pattern"] { + table[facet] = ["value", "id"] + } + return table + }() + + /// Findings for the unprefixed attributes on `node`: any not admitted by its + /// component, and the `ref` exclusions (`ref` excludes `name` and `type`). A + /// prefixed (foreign-namespace) attribute and a namespace declaration are not + /// the component's own and are not checked. + private static func attributeApplicabilityErrors(_ node: XSDTree, local: String) -> [String] { + guard let allowed = allowedAttributes[local] else { return [] } + var errors: [String] = [] + var present: Set = [] + for attribute in node.attributes where attribute.name.prefix == nil && attribute.name.localName != "xmlns" { + let name = attribute.name.localName + present.insert(name) + if !allowed.contains(name) { + errors.append("attribute '\(name)' is not allowed on '\(local)'") + } + } + if present.contains("ref"), present.contains("name") { + errors.append("'\(local)' may not have both 'ref' and 'name'") + } + if present.contains("ref"), present.contains("type") { + errors.append("'\(local)' may not have both 'ref' and 'type'") + } + return errors + } + private static func collectStructure(_ node: XSDTree, into errors: inout [String]) { let local = PureXML.Schema.XSDNode.localName(node) if local == "appinfo" || local == "documentation" { return } @@ -66,6 +137,7 @@ extension PureXML.Schema.XSDParser { if node.name?.namespaceURI == xsdNamespace { errors += attributeValueErrors(node) errors += occurrenceOrderErrors(node) + if let local { errors += attributeApplicabilityErrors(node, local: local) } let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName) if let local, let allowed = allowedChildren[local] { errors += childErrors(local: local, children: names, allowed: allowed) diff --git a/Tests/SchemaStructureTests.swift b/Tests/SchemaStructureTests.swift index 9115426..df6c8b5 100644 --- a/Tests/SchemaStructureTests.swift +++ b/Tests/SchemaStructureTests.swift @@ -180,6 +180,21 @@ struct SchemaStructureTests { ) } + @Test("an attribute not admitted by a component is rejected") + func test_attributeApplicability() { + #expect(rejects(#""#)) // typo for nillable + #expect(rejects(#""#)) + #expect(rejects(#""#)) // sequence has no name + // A foreign-namespace attribute is allowed. + try? compile(#""#) + } + + @Test("ref excludes name and type") + func test_refExclusions() { + #expect(rejects(#""#)) + #expect(rejects(#""#)) + } + @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 4c69291..4d4d6d9 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 = 956 + private let knownSchemaInvalidAccepted = 925 private let knownInstanceValidRejected = 214 private let knownInstanceInvalidAccepted = 165 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index 4cb0727..7263f4d 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 -> 956** | we rarely catch a bad one (facet, id, and structural content-model validity landed) | +| **invalid-schemas-accepted** | **2461 -> 925** | 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). **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). **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 ace179d..8f04ec4 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-five root causes, ~2179 deviations cleared) +### Fixed (twenty-six root causes, ~2210 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 956 (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 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`. Plus the measurement fix (exclude W3C-disputed `status="queried"` entries). ### Remaining, root-caused (the hard tail)