PureXML/Sources/Schema/SchemaIDAttribute.swift
Mihaela Mihaljevic 1249dd4c14 feat(schema): validate the id attribute (xs:ID) at compile time
The id attribute on any XSD component is of type xs:ID, so each value must be a
valid NCName and unique within the schema document. The values were never
checked, so a malformed (id="", id="123") or duplicated id left the schema
wrongly accepted across many categories (idA-idK, attribute groups, complex
types).

The schema-document tree is walked once, collecting id attributes. Two guards
keep valid schemas from being rejected: appinfo/documentation subtrees are
skipped (their foreign content carries its own non-xs:ID id attributes), and
only the unprefixed id is treated as the component identifier (a foreign xml:id
or pre:id is a different attribute). Findings join the shared parse-time
diagnostics.

XSTS: invalid schemas accepted 1789 to 1688; no other bucket raised.
2026-06-13 22:29:29 +02:00

42 lines
2.1 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
}
}