PureXML/Sources/Schema/SchemaIDAttribute.swift
Mihaela Mihaljevic 489275c6b8 feat(schema): locate compile diagnostics with validation coding paths (#169)
Schema compile findings now carry ValidationError coding paths that resolve to source spans on a ranged tree, enabling IDE underlines for id, structure, include, and facet violations.
2026-06-16 10:26:11 +02:00

83 lines
3.8 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] {
idAttributeFindings(schema).map(\.reason)
}
static func idAttributeFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
var seen: Set<String> = []
collectIDFindings(schema, into: &findings, seen: &seen)
return findings
}
private static func collectIDFindings(
_ node: XSDTree,
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
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) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "id attribute value '\(id)' is not a valid NCName",
node: node,
))
} else if !seen.insert(id).inserted {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "duplicate id attribute value '\(id)' in the schema document",
node: node,
))
}
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
collectIDFindings(child, into: &findings, 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 before named types resolve.
static func consistencyErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ containers: [XSDTree]) -> [String] {
PureXML.Validation.SchemaCompile.preCompileErrors(schema: schema, context: context, containers: containers).map(\.reason)
}
/// Consistency findings that depend on the resolved named types.
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] {
PureXML.Validation.SchemaCompile.postCompileErrors(
schema: schema,
context: context,
containers: containers,
namedTypes: PureXML.Schema.SchemaCompileNamedTypes(
derivation: derivation,
globalElements: typeMaps.global,
types: typeMaps.named,
),
).map(\.reason)
}
// Legacy helpers retained for direct unit tests of individual checks.
}