feat(schema): enforce schema-vocabulary attribute applicability
The attribute sibling of the content-model table. Each XSD component admits a fixed set of attributes; an unprefixed attribute outside that set (a typo like nullable, a stray foo, name on a sequence) was accepted because nothing checked attribute applicability. allowedAttributes now lists each component's admitted unprefixed attributes (the permissive union of global and local use), and attributeApplicabilityErrors rejects the rest, plus ref co-occurring with name or type. Foreign (prefixed) attributes and namespace declarations are the schema author's own and are not checked, so no valid schema is rejected. XSTS: invalid schemas accepted 956 to 925 (-31); valid schemas rejected held at 72; instance buckets unchanged.
This commit is contained in:
parent
b37e97b5a9
commit
39222ca760
@ -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.
|
||||
|
||||
@ -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<String>] = {
|
||||
let facetAttrs: Set = ["value", "fixed", "id"]
|
||||
var table: [String: Set<String>] = [
|
||||
"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<String> = []
|
||||
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)
|
||||
|
||||
@ -180,6 +180,21 @@ struct SchemaStructureTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test("an attribute not admitted by a component is rejected")
|
||||
func test_attributeApplicability() {
|
||||
#expect(rejects(#"<xs:element name="a" type="xs:string" nullable="true"/>"#)) // typo for nillable
|
||||
#expect(rejects(#"<xs:complexType name="t" foo="bar"><xs:sequence/></xs:complexType>"#))
|
||||
#expect(rejects(#"<xs:sequence name="x"/>"#)) // sequence has no name
|
||||
// A foreign-namespace attribute is allowed.
|
||||
try? compile(#"<xs:element name="a" type="xs:string" xmlns:x="urn:x" x:note="hi"/>"#)
|
||||
}
|
||||
|
||||
@Test("ref excludes name and type")
|
||||
func test_refExclusions() {
|
||||
#expect(rejects(#"<xs:complexType name="t"><xs:sequence><xs:element ref="g" name="h"/></xs:sequence></xs:complexType><xs:element name="g" type="xs:string"/>"#))
|
||||
#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("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 = 956
|
||||
private let knownSchemaInvalidAccepted = 925
|
||||
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 -> 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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user