PureXML/Sources/Schema/SchemaIDAttribute.swift
Mihaela Mihaljevic aa6e76da88 feat(schema): validate value constraints against user types; fix inline restriction base
A default/fixed value on an element or attribute whose type names a user-declared
type in the schema's own target namespace must be a valid value of that type
(XSD 1.0 e-props-correct.2 / a-props-correct.2). The check runs after the named
types are compiled and validates with the compiled SimpleType validator (the one
instance validation uses), so whitespace and facet handling match. Built-ins are
handled at structure time; imported, foreign, and inline anonymous types are left
alone via a namespace gate.

This exposed a pre-existing compile defect: a simpleType restriction read its base
only from the base attribute, so an inline anonymous <simpleType> base (a list or
union with no base attribute) was dropped and the type collapsed to an atomic
string, making a length facet count characters instead of list items. The
restriction branch now resolves the inline child as its base, preserving list and
union variety. This corrects both schema-time value checking and instance
validation.

XSTS: invalid schemas accepted 462 to 454; invalid instances accepted 160 to 158;
no other bucket raised.
2026-06-15 14:06:26 +02:00

75 lines
4.2 KiB
Swift

extension PureXML.Schema.XSDParser {
/// Schema-validity findings for the `id` attributes in a schema document. The
/// `id` attribute on any XSD component is of type `xs:ID`, so each value must
/// be a valid NCName and all values must be unique within the document
/// (XSD Structures: `xs:ID` and the ID/IDREF constraints). The values were
/// never checked, so a malformed (`id=""`, `id="123"`) or duplicated `id` left
/// the schema wrongly accepted.
///
/// Walks the one document rooted at `schema`; an included or imported document
/// keeps its own `id` scope, so cross-document collisions are not flagged here.
static func idAttributeErrors(_ schema: XSDTree) -> [String] {
var errors: [String] = []
var seen: Set<String> = []
collectIDErrors(schema, into: &errors, seen: &seen)
return errors
}
private static func collectIDErrors(_ node: XSDTree, into errors: inout [String], seen: inout Set<String>) {
// `appinfo` and `documentation` hold arbitrary foreign content whose own
// `id` attributes are not xs:ID; do not descend into them.
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" { return }
if let id = unprefixedID(node) {
if !PureXML.Schema.Lexical.isNCName(id) {
errors.append("id attribute value '\(id)' is not a valid NCName")
} else if !seen.insert(id).inserted {
errors.append("duplicate id attribute value '\(id)' in the schema document")
}
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
collectIDErrors(child, into: &errors, seen: &seen)
}
}
/// The value of the unprefixed, no-namespace `id` attribute (the xs:ID one),
/// or nil. A prefixed attribute such as `xml:id` or a foreign `pre:id` is a
/// different attribute and is not the schema-component identifier.
private static func unprefixedID(_ node: XSDTree) -> String? {
node.attributes.first { $0.name.prefix == nil && $0.name.localName == "id" }?.value
}
/// All compile-time schema-consistency findings, collected together so they are
/// reported in one pass: `id` validity, schema-for-schemas structure, global
/// component-name uniqueness, content-model determinism (UPA), and circular
/// type derivation. The explicit return type keeps the type-checker from
/// inferring a long concatenation.
static func consistencyErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ containers: [XSDTree]) -> [String] {
let structural = idAttributeErrors(schema) + structureErrors(schema) + componentNameErrors(schema)
let determinism = PureXML.Schema.ContentModelDeterminism.violations(in: schema, context: context)
let cycles = derivationCycleErrors(containers, context.namespaceBindings, context.targetNamespace)
+ circularReferenceErrors(containers, context.namespaceBindings, context.targetNamespace)
let placement = allGroupReferencePlacementErrors(containers, context.namespaceBindings, context.targetNamespace)
return structural + determinism + cycles + placement
}
/// Consistency findings that depend on the resolved named types, collected after
/// `namedTypes` has populated them: unresolved references, attribute-use
/// uniqueness and single-ID, ID-typed value constraints, substitution-group
/// member type derivation (`e-props-correct.4`), and value constraints valid
/// against a user type (`e-props-correct.2` / `a-props-correct.2`).
static func postNamedTypeErrors(
_ schema: XSDTree,
_ context: PureXML.Schema.XSDContext,
_ containers: [XSDTree],
_ derivation: DerivationTables,
typeMaps: (global: [String: PureXML.Schema.ElementType], named: [String: PureXML.Schema.ElementType]),
) -> [String] {
referenceErrors(schema, in: context, elements: typeMaps.global)
+ attributeUseErrors(containers, context)
+ idValueConstraintErrors(schema, context)
+ substitutionTypeErrors(schema, derivation, typeMaps.named)
+ userTypeValueConstraintErrors(schema, context, typeMaps.named)
}
}