fix(schema): reject an attribute reference whose fixed value contradicts its global (#145)

An attribute use that references a global attribute carrying a `fixed` value
constraint may not contradict it (au-props-correct.2): a `fixed` on the
reference whose value differs from the global's, or a `default` on a reference
to a fixed attribute, is invalid. A reference that omits its own value
constraint inherits the global's and stays valid.

The two `fixed` values are compared in the attribute type's value space, so a
value-equal pair written in different lexical forms (`01` for `1`) is not
flagged. The global is resolved by local name and only checked once its own
namespace matches the reference's resolved namespace, so a same-local-name
attribute imported from another namespace cannot surface as the wrong global
and contradict a valid reference.

Caught two invalid schemas: XSTS invalid-schemas-accepted 232 -> 230, no other
bucket moved.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 13:43:42 +02:00
parent a96e5ce3ac
commit c80a8e06c4
3 changed files with 42 additions and 1 deletions

View File

@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
### Fixed
- XSTS invalid-schemas-accepted (#145): an attribute use that references a global attribute carrying a `fixed` value constraint may not contradict it. A `fixed` on the reference whose value differs from the global's (compared in the attribute type's value space, so `1` and `01` are not wrongly flagged), or a `default` on a reference to a fixed global, is invalid (`au-props-correct.2`). A reference that omits its own value constraint inherits the global's fixed value and stays valid. Caught two invalid schemas: invalid-schemas-accepted 232 → 230, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a `complexContent` extension that adds element content may not extend a base whose whole content is a non-empty `all` group; the base's `all` would be nested in the sequence joining base and extension content (`cos-all-limited`). An empty-`all` base, or an attribute-only extension, stays valid. Caught one invalid schema: invalid-schemas-accepted 233 → 232, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a `complexContent` extension that adds element content (a non-empty model group) may not extend a base whose content is simple (`cos-ct-extends.1.4.2.2`); a simple-content type is extended through `simpleContent`. An attribute-only `complexContent` extension of a simpleContent base stays valid (the `complexContent`-around-`simpleContent` idiom). Caught two invalid schemas: invalid-schemas-accepted 235 → 233, no other bucket moved.

View File

@ -34,6 +34,41 @@ extension PureXML.Schema.XSDParser {
let label = AttrNode.attribute(type, "name").map { "complex type '\($0)'" } ?? "an anonymous complex type"
errors += attributeSetErrors(attributeDeclarations(under: attributeContainer(of: type), context), label)
}
errors += attributeRefValueConstraintErrors(container, context)
}
return errors
}
/// XSD 1.0 `au-props-correct.2`: when an attribute use references a global
/// attribute that has a `fixed` value constraint, the use may not contradict
/// it: a `fixed` whose value differs, or a `default`, on the reference is
/// invalid (the use either inherits the fixed value or restates the same one).
/// Compared in the attribute type's value space, so a value-equal pair written
/// in different lexical forms (`1` and `01`) is not wrongly flagged.
///
/// The global declaration is looked up by local name (the only key the global
/// pool carries), so a reference is checked against it only once the resolved
/// declaration's own namespace matches the reference's resolved namespace. That
/// rejects a same-local-name collision across namespaces (an imported `imp:a`
/// versus a local `a`), where the local-name pool could otherwise surface the
/// wrong global and contradict a valid reference.
private static func attributeRefValueConstraintErrors(_ container: XSDTree, _ context: PureXML.Schema.XSDContext) -> [String] {
var errors: [String] = []
for use in descendants(container, named: "attribute") {
guard let ref = AttrNode.attribute(use, "ref"),
let declaration = context.globalAttributes[AttrNode.stripPrefix(ref)],
let resolved = attributeUse(declaration, context),
resolved.name.namespaceURI == AttrNode.referenceNamespace(ref, context.namespaceBindings),
let globalFixed = AttrNode.attribute(declaration, "fixed")
else { continue }
if let useFixed = AttrNode.attribute(use, "fixed") {
let equal = resolved.type.valueMatches(useFixed, literal: globalFixed) || useFixed == globalFixed
if !equal {
errors.append("attribute reference to '\(AttrNode.stripPrefix(ref))' must use its fixed value '\(globalFixed)', not '\(useFixed)'")
}
} else if AttrNode.attribute(use, "default") != nil {
errors.append("attribute reference to '\(AttrNode.stripPrefix(ref))' has a fixed value and may not specify a default")
}
}
return errors
}

View File

@ -87,8 +87,12 @@ struct XSTSSuiteTests {
/// base whose whole content is an `all` group (the all would be nested in the
/// joining sequence; cos-all-limited) then caught one more invalid schema:
/// invalid-schemas accepted 233 → 232, no other bucket moved.
/// Rejecting an attribute use that references a global attribute with a `fixed`
/// value constraint but contradicts it (a value-space-differing `fixed`, or a
/// `default`; au-props-correct.2) then caught two more invalid schemas:
/// invalid-schemas accepted 232 → 230, no other bucket moved.
private let knownSchemaValidRejected = 1
private let knownSchemaInvalidAccepted = 232
private let knownSchemaInvalidAccepted = 230
private let knownInstanceValidRejected = 3
private let knownInstanceInvalidAccepted = 132