fix(schema): an attribute's type must be a simple type (au-props-correct); invalid-schemas-accepted 96 to 95 (#145)

The {type definition} of an attribute is a simple type (au-props-correct /
src-attribute). An attribute whose type resolves to a complex type, or to the
complex ur-type xs:anyType, is now rejected; a built-in or user simple type,
an inline simpleType, or no type (anySimpleType) stays valid. A locally-
declared type takes precedence over the built-in reading, consistent with the
src-ct rules.

Strictly zero false positive: an attribute can never carry a complex type, so
no valid schema is affected. XSTS: invalid-schemas-accepted 96 to 95 (attD002),
valid-schemas-rejected held at 0, no other bucket moved. Differential:
PureXML-stricter unchanged at 87 over 9618 schemas.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 22:12:01 +02:00
parent a2047956cb
commit 615f668fc2
6 changed files with 57 additions and 3 deletions

View File

@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- XSTS invalid-schemas-accepted (#145): an attribute's type must be a simple type (`au-props-correct` / `src-attribute`). An `attribute` whose `type` resolves to a complex type, or to the complex ur-type `xs:anyType`, is rejected; a built-in or user simple type, an inline `simpleType`, or no type (`anySimpleType`) stays valid. A locally-declared type takes precedence over the built-in reading. invalid-schemas-accepted 96 → 95, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.
- XSTS invalid-schemas-accepted (#145): an element's `default`/`fixed` value constraint requires a content type that admits character data (`cos-valid-default`). An element whose type is, or whose inline complex type has, element-only or empty content may not carry a value constraint; a simple type, mixed content, the ur-type `xs:anyType`, and an untyped element (also `anyType`) stay valid. The check flags only a content type that is certainly element-only or empty, so it cannot over-reject. invalid-schemas-accepted 99 → 96, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.
- XSTS invalid-schemas-accepted (#145): the base-type kind of a content-model derivation is enforced. A `complexContent` restriction or extension base must be a complex type (`src-ct.1`), so a `simpleType` or a built-in simple type such as `xs:string` is rejected (the ur-type `xs:anyType` is complex and stays valid). A `simpleContent` extension base must be a simple type or a complex type with simple content (`src-ct.2`), so a complex type with element-only, mixed, or empty content, including `xs:anyType`, is rejected. A locally-declared type takes precedence over the built-in reading, so a schema that targets the XSD namespace and extends its own complex types (the schema-for-schemas) is not rejected. invalid-schemas-accepted 104 → 99, no false positive (valid-schemas-rejected held at 0, corroborated by the differential: PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved.

View File

@ -10,6 +10,32 @@ private struct ReachedAttribute {
}
extension PureXML.Schema.XSDParser {
/// XSD 1.0 `au-props-correct` / `src-attribute`: the {type definition} of an
/// attribute is a SIMPLE type. An `attribute` whose `type` resolves to a complex
/// type, or to the complex ur-type `xs:anyType`, is invalid; a built-in or user
/// simple type, an inline `simpleType`, or no type (`anySimpleType`) is valid. A
/// locally-declared type takes precedence over the built-in reading (a schema may
/// target the XSD namespace and define its own components there), so resolve
/// `types` first. Self-contained schema; a foreign or unresolved type is left
/// alone. Strictly zero false positive: an attribute can never carry a complex
/// type, so no valid schema is affected.
static func attributeTypeMustBeSimpleErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
let bindings = context.namespaceBindings
var errors: [String] = []
for attribute in descendants(schema, named: "attribute") where attribute.name?.namespaceURI == xsdNamespace {
guard let typeName = AttrNode.attribute(attribute, "type") else { continue }
let namespace = AttrNode.referenceNamespace(typeName, bindings)
let local = AttrNode.stripPrefix(typeName)
if namespace == context.targetNamespace, case .complex? = types[local] {
errors.append("an attribute's type must be a simple type, not the complex type '\(local)'")
} else if namespace == xsdNamespace, types[local] == nil, local == "anyType" {
errors.append("an attribute's type must be a simple type, not 'anyType'")
}
}
return errors
}
/// Schema-validity findings for attribute-use uniqueness (XSD 1.0
/// `ct-props-correct.4` / `ag-props-correct.2`) and the single-ID rule
/// (`ct-props-correct.5`): a complex type's or attribute group's complete

View File

@ -262,7 +262,7 @@ extension PureXML.Validation {
static func postCompileValidator() -> Validator<PureXML.Schema.SchemaCompileContext> {
Validator<PureXML.Schema.SchemaCompileContext>.defaults(
nonReference: [
AnyValidation(attributeUsesValid),
AnyValidation(attributeUsesValid), AnyValidation(attributeTypesSimple),
AnyValidation(idValueConstraintsValid),
AnyValidation(userTypeValueConstraintsValid),
AnyValidation(extensionAllGroupsValid),
@ -341,6 +341,16 @@ extension PureXML.Validation {
}
extension PureXML.Validation.SchemaCompile {
/// au-props-correct: an attribute's type is a simple type, never complex.
static var attributeTypesSimple: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("An attribute's type is a simple type") { document in
guard let namedTypes = document.namedTypes else { return [] }
return PureXML.Schema.SchemaLocatedFinding.unlocated(
PureXML.Schema.XSDParser.attributeTypeMustBeSimpleErrors(document.schema, document.context, namedTypes),
)
}
}
/// Particle Valid (Restriction) for anonymous (inline) complex types, which the
/// name-keyed `restrictionsAreSubsets` rule never sees.
static var anonymousRestrictionsValid: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {

View File

@ -44,4 +44,18 @@ struct SchemaTopLevelAttributeTests {
+ "</xs:complexType></xs:element>",
))
}
/// au-props-correct: an attribute's type must be a simple type. A complex type
/// (here one with simple content) is rejected; a built-in or user simple type,
/// an inline simpleType, or no type (anySimpleType) is valid.
@Test("an attribute's type must be a simple type")
func test_attributeTypeMustBeSimple() {
let complexT = "<xs:complexType name=\"ct\"><xs:simpleContent><xs:extension base=\"xs:integer\"/></xs:simpleContent></xs:complexType>"
#expect(!compiles(complexT + "<xs:attribute name=\"a\" type=\"ct\"/>"))
#expect(!compiles("<xs:attribute name=\"a\" type=\"xs:anyType\"/>"))
let simpleT = "<xs:simpleType name=\"st\"><xs:restriction base=\"xs:string\"/></xs:simpleType>"
#expect(compiles(simpleT + "<xs:attribute name=\"a\" type=\"st\"/>"))
#expect(compiles("<xs:attribute name=\"a\"><xs:simpleType><xs:restriction base=\"xs:string\"/></xs:simpleType></xs:attribute>"))
#expect(compiles("<xs:attribute name=\"a\"/>"))
}
}

View File

@ -97,6 +97,7 @@ struct ValidationConfigurationTests {
])
#expect(PureXML.Validation.SchemaCompile.postCompileValidator().validationDescriptions == [
"Attribute uses are unique and declare at most one ID attribute",
"An attribute's type is a simple type",
"ID-typed value constraints are valid",
"Element value constraints are valid against their declared types",
"Complex types extending xs:all groups satisfy XSD placement rules",

View File

@ -151,9 +151,10 @@ struct XSTSSuiteTests {
/// a local type takes precedence over the built-in reading so the schema-for-
/// schemas, which targets the XSD namespace, is not rejected) 104 -> 99; and the
/// element value-constraint content type (cos-valid-default: a default/fixed needs
/// simple or mixed content, not element-only) 99 -> 96.
/// simple or mixed content, not element-only) 99 -> 96; and an attribute's type
/// must be a simple type, never a complex type (au-props-correct) 96 -> 95.
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 96
private let knownSchemaInvalidAccepted = 95
private let knownInstanceValidRejected = 0
private let knownInstanceInvalidAccepted = 133