PureXML/Sources/Schema/SchemaAllGroupPlacement.swift
Mihaela Mihaljevic ee5c46929c feat(schema): locate derivation/cycle-pass schema diagnostics
Convert the three derivation/cycle-pass compile checkers to located findings
so each error attaches to its declaring node instead of the schema root
(#169): an all-group referenced inside a sequence/choice/all locates on the
offending group reference (allGroupReferencePlacementFindings); a type
derived from itself locates on its complexType/simpleType
(derivationCycleFindings); and an element that is a member of its own
substitution group, or a model group or attribute group that references
itself, locates on its declaration (circularReferenceFindings).

The cycle checkers report a cycle by component name over a name-keyed graph,
so each now records the declaring node in a parallel nodeOf map built in the
same loop as the graph; a name that yields a finding always has a node, so a
mutual cycle underlines each member on its own declaration. Every predicate,
key ordering, and reason string is unchanged; only the location each finding
carries differs. The three checkers had exactly one caller each, so each is
renamed in place and the .unlocated wrap dropped. Adds a located-diagnostics
test per checker family, covering both substitution and group-reference
cycles.
2026-06-21 16:53:02 +02:00

83 lines
4.5 KiB
Swift

private typealias AllNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// XSD 1.0 `cos-all-limited.1.2`: an `all` group may only be the whole content
/// model of a complex type. A direct `all` nested in a `sequence`/`choice` is
/// already rejected by the child-content table; the remaining case is an `all`
/// reached through a `<group ref>` to a named group whose content is an `all`,
/// where that reference appears inside a compositor (so the `all` is not the
/// whole content). The reference is followed only when it resolves to this
/// schema's own target namespace.
static func allGroupReferencePlacementFindings(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [PureXML.Schema.SchemaLocatedFinding] {
// A redefined group's effective content is the redefinition's, not the
// original loaded definition, so a redefined group name is exempt: its
// original `all` content is overridden (and the suite treats a reference to
// it inside a compositor as valid).
var redefined: Set<String> = []
for container in containers where AllNode.localName(container) == "redefine" {
for group in AllNode.children(container, named: "group") {
if let name = AllNode.attribute(group, "name") { redefined.insert(name) }
}
}
var allGroups: Set<String> = []
for container in containers {
// A named group defines a component in the target namespace of the
// schema document that holds it. A reference is only ever followed when
// it resolves to this schema's own `target` (see `allGroupReference`),
// so pooling a same-named all-group from an *imported* namespace would
// wrongly flag a local non-all group referenced inside a compositor.
// Record only all-groups defined in `target`: a `schema` container's
// own namespace is its `targetNamespace`; a `redefine` container's
// redefinitions belong to the including schema (this `target`).
let containerNamespace = AllNode.localName(container) == "redefine"
? target
: AllNode.attribute(container, "targetNamespace")
guard containerNamespace == target else { continue }
for group in descendants(container, named: "group") {
guard let name = AllNode.attribute(group, "name"), !redefined.contains(name) else { continue }
if AllNode.elementChildren(group).contains(where: { AllNode.localName($0) == "all" }) {
allGroups.insert(name)
}
}
}
guard !allGroups.isEmpty else { return [] }
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
for container in containers {
collectAllGroupRefPlacement(container, allGroups, bindings, target, into: &findings)
}
return findings
}
private static func collectAllGroupRefPlacement(
_ node: XSDTree,
_ allGroups: Set<String>,
_ bindings: [String: String],
_ target: String?,
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
) {
let kind = AllNode.localName(node)
let insideCompositor = kind == "sequence" || kind == "choice" || kind == "all"
for child in AllNode.elementChildren(node) {
if insideCompositor, let referenced = allGroupReference(child, allGroups, bindings, target) {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "the all-group '\(referenced)' may not be referenced inside a '\(kind ?? "")'; an all group must be the whole content model",
node: child,
))
}
collectAllGroupRefPlacement(child, allGroups, bindings, target, into: &findings)
}
}
/// The name of the all-group `child` references in this schema's target
/// namespace, or nil when `child` is not such a reference.
private static func allGroupReference(_ child: XSDTree, _ allGroups: Set<String>, _ bindings: [String: String], _ target: String?) -> String? {
guard AllNode.localName(child) == "group", let ref = AllNode.attribute(child, "ref"),
AllNode.referenceNamespace(ref, bindings) == target
else {
return nil
}
let name = AllNode.stripPrefix(ref)
return allGroups.contains(name) ? name : nil
}
}