fix(schema): reject identity field whose declared target is a non-simple complex type
cvc-identity-constraint.3 / c-fields-xpaths require an identity-constraint field to identify a node with a simple type. The prior fix rejected a field whose selected instance node carries element children (structural). This adds the case where the field's DECLARED type is a complex type with non-simple content (empty or element-only/mixed): an attributes-only element resolves to an empty-content complex type, so its instance node is childless yet not simple-typed. The field's target type is resolved through the schema type model (the selector target's declared type, then the field's same-named child element in its content model) and classified. A simple or simpleContent target, a type reached only through a named reference, a complex field path, or an unresolvable target is left alone, so no valid field is rejected. The resolution helpers live in a separate extension file to keep the validator within the type-body limit. This catches W3C XSTS idG006. XSTS invalid-instances-accepted 23 -> 22, valid-instances-rejected held at 0.
This commit is contained in:
parent
18fd42b726
commit
74b9332c48
@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- An identity-constraint field whose DECLARED target is a complex type with non-simple content is now rejected even when the instance node has no element children. The earlier fix rejected a field whose selected node carries element children (structural); this adds the case where the field's declared type is an empty or element-only/mixed complex type (an attributes-only element resolves to an empty-content complex type, so its instance node is childless yet not simple-typed). The field's target type is resolved through the schema type model (the selector target's type, then the field's same-named child element); a simple or simpleContent target, a type reached only through a named reference, a complex field path, or an unresolvable target is left alone, so no valid field is rejected. Catches idG006. XSTS invalid-instances-accepted 23 → 22, valid-instances-rejected held at 0.
|
||||
- A substitution-group `block` on a head element now bars substituting a member whose INLINE (anonymous) type reaches the head's type by a blocked method. The substitution-group filter already dropped a member when the head's `{disallowed substitutions}` (or the head type's `{prohibited substitutions}`) blocked the member's derivation method, but only for members with a NAMED type recorded in the derivation backbone; an element with an inline complex type had no backbone entry, so a member extending the head's type was wrongly kept even when the head blocked `extension`. Inline-typed elements now record their derivation under a synthetic key (an element key plus a `#` marker no QName can contain, so it cannot collide with a real type and is unreachable as any other derivation base), so the filter sees the inline member's method. A member reaching the head's type by an UNblocked method, a plain inline type with no derivation, and same-local-name elements in different namespaces are all unaffected. Catches disallowedSubst00105m (and a sibling). XSTS invalid-instances-accepted 25 → 23, valid-instances-rejected held at 0.
|
||||
- An identity-constraint (`key`/`unique`/`keyref`) field that identifies a node with complex content is now rejected during instance validation. Per cvc-identity-constraint.3 / c-fields-xpaths a field must evaluate to a node with a simple type (a simple-typed element, an element with simple content, or an attribute); the cardinality clause (at most one node) was enforced, but the simple-content clause was not. The check is structural and false-positive-free: a field whose selected node is an element carrying element children has complex (element-only or mixed) content and can never be simple-typed, so it is flagged, while a simple-typed or simple-content element (text only), an empty or text-only node, and an attribute are left alone. Catches idK012. XSTS invalid-instances-accepted 26 → 25, valid-instances-rejected held at 0.
|
||||
- The attribute-use uniqueness check (`ag-props-correct.2` / `ct-props-correct.4`) now flattens a redefined attribute group's self-reference and excludes prohibited uses. When an attribute group flattens its uses through `<attributeGroup ref>` references, a reference to a redefined group whose redefinition self-references its former self previously stopped at the cycle guard, so the original's attributes were never reached and a clash they cause was missed. The self-reference now expands to the group's pre-redefine base definition (already retained for this purpose), so the original's attributes join the set; a genuine non-redefine cycle still terminates without contributing. Separately, a `use="prohibited"` attribute is now excluded from the duplicate-name and single-ID counts, since a prohibited use is a removal marker, not a member of `{attribute uses}` (XSD §3.4.2/§3.6.2); this also keeps a self-reference that re-imports an attribute and then prohibits it from reading as a duplicate. Catches attQ011 and attQ017 (an attribute group referencing both a global attribute and a redefined self-referencing group that supplies the same-named attribute). XSTS invalid-schemas-accepted 22 → 20, valid-schemas-rejected held at 0.
|
||||
|
||||
44
Sources/Schema/IdentityFieldType.swift
Normal file
44
Sources/Schema/IdentityFieldType.swift
Normal file
@ -0,0 +1,44 @@
|
||||
extension PureXML.Schema.IdentityValidator {
|
||||
/// Whether the field's DECLARED target is a complex type with non-simple content
|
||||
/// (empty or element-only/mixed), which is not a valid identity field even when its
|
||||
/// instance node has no element children (an attributes-only element, XSTS idG006).
|
||||
/// Resolved through the schema type model: the selector target's type is looked up,
|
||||
/// the field's same-named child element found in its content model, and its declared
|
||||
/// type classified. A simple/simpleContent type, a type reached only through a
|
||||
/// reference, a complex field path, or an unresolvable target is left alone, so no
|
||||
/// valid field is rejected (cvc-identity-constraint.3 / c-fields-xpaths).
|
||||
func fieldTargetIsComplexNonSimple(_ field: String, at target: PureXML.Model.TreeNode) -> Bool {
|
||||
guard !field.contains(where: { "@/[(.".contains($0) }), let targetName = target.name,
|
||||
case let .complex(targetComplex)? = declaredType(of: targetName),
|
||||
let fieldType = childElementType(named: field, in: targetComplex.content),
|
||||
case let .complex(fieldComplex) = fieldType
|
||||
else { return false }
|
||||
if case .simpleContent = fieldComplex.content { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
/// The declared `ElementType` of a global element, by namespaced or local key.
|
||||
private func declaredType(of name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
||||
types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] ?? types[PureXML.Schema.XSDParser.elementKey(name.localName)]
|
||||
}
|
||||
|
||||
/// The declared type of the first element named `local` directly within a content
|
||||
/// model (descending through model groups), or nil when none matches.
|
||||
private func childElementType(named local: String, in content: PureXML.Schema.ContentType) -> PureXML.Schema.ElementType? {
|
||||
switch content {
|
||||
case let .elementOnly(particle), let .mixed(particle): childElementType(named: local, in: particle)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
private func childElementType(named local: String, in particle: PureXML.Schema.Particle) -> PureXML.Schema.ElementType? {
|
||||
switch particle.term {
|
||||
case let .element(name, type, _, _, _, _):
|
||||
name.localName == local ? type : nil
|
||||
case let .group(group):
|
||||
group.particles.lazy.compactMap { childElementType(named: local, in: $0) }.first
|
||||
case .wildcard:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -205,11 +205,14 @@ extension PureXML.Schema {
|
||||
}
|
||||
// cvc-identity-constraint.3 / c-fields-xpaths: a field must identify a node
|
||||
// with a simple type (or simple content). An element with element children
|
||||
// has complex content and can never be simple-typed, so it is not a valid
|
||||
// field target. Checked structurally: a simple-typed or simple-content
|
||||
// element carries only text, never element children, so no valid field is
|
||||
// rejected; an empty or text-only node, or an attribute, is left alone.
|
||||
if let node = selected.first, node.kind == .element, node.children.contains(where: { $0.kind == .element }) {
|
||||
// has complex content (checked structurally), and an element whose DECLARED
|
||||
// type is a complex type with non-simple content (empty or element-only, so
|
||||
// an attributes-only element with no children, XSTS idG006) is equally
|
||||
// invalid (checked through the schema type model). A simple-typed or
|
||||
// simple-content element, a text-only node, and an attribute are left alone.
|
||||
let element = selected.first.flatMap { $0.kind == .element ? $0 : nil }
|
||||
let hasElementChildren = element?.children.contains { $0.kind == .element } ?? false
|
||||
if element != nil, hasElementChildren || fieldTargetIsComplexNonSimple(field, at: target) {
|
||||
return .init(
|
||||
reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content",
|
||||
at: targetPath + step,
|
||||
|
||||
75
Tests/SchemaIdentityFieldTypeTests.swift
Normal file
75
Tests/SchemaIdentityFieldTypeTests.swift
Normal file
@ -0,0 +1,75 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// cvc-identity-constraint.3 / c-fields-xpaths: an identity-constraint field must
|
||||
/// identify a node with a simple type (a simple-typed element, an element with simple
|
||||
/// content, or an attribute). A field whose target is a complex type with non-simple
|
||||
/// content is invalid, whether the instance node has element children (idK012) or is
|
||||
/// empty/attributes-only (idG006). A simple or simpleContent target stays valid.
|
||||
@Suite("XSD identity-constraint field simple-content")
|
||||
struct SchemaIdentityFieldTypeTests {
|
||||
private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.ValidationError] {
|
||||
try PureXML.Schema.Document(xsd).validate(xml)
|
||||
}
|
||||
|
||||
private let head = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
|
||||
|
||||
@Test("a field selecting a complex-content element is rejected (idK012)")
|
||||
func test_fieldWithElementChildrenRejected() throws {
|
||||
let complexField = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence>
|
||||
<xs:element name="uid" maxOccurs="unbounded"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid"><xs:complexType><xs:sequence><xs:element name="gid"/></xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType>
|
||||
<xs:key name="k"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try !validate(complexField, "<root><uid><pid><gid>x</gid></pid></uid></root>").isEmpty)
|
||||
let simpleField = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence>
|
||||
<xs:element name="uid" maxOccurs="unbounded"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid" type="xs:string"/>
|
||||
</xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType>
|
||||
<xs:key name="k"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(simpleField, "<root><uid><pid>a</pid></uid><uid><pid>b</pid></uid></root>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("a field whose declared type is an empty/attributes-only complex type is rejected (idG006)")
|
||||
func test_emptyComplexFieldRejected() throws {
|
||||
let emptyComplex = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence><xs:element ref="uid" maxOccurs="unbounded"/></xs:sequence></xs:complexType>
|
||||
<xs:key name="uuid"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
<xs:element name="uid"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid"><xs:complexType><xs:attribute name="p" type="xs:string"/></xs:complexType></xs:element>
|
||||
</xs:sequence><xs:attribute name="val" type="xs:string"/></xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try !validate(emptyComplex, "<root><uid val=\"1\"><pid p=\"11\"/></uid></root>").isEmpty)
|
||||
let simpleContent = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence><xs:element ref="uid" maxOccurs="unbounded"/></xs:sequence></xs:complexType>
|
||||
<xs:key name="uuid"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
<xs:element name="uid"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid"><xs:complexType><xs:simpleContent><xs:extension base="xs:string">
|
||||
<xs:attribute name="p" type="xs:string"/></xs:extension></xs:simpleContent></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(simpleContent, "<root><uid><pid p=\"1\">a</pid></uid></root>").isEmpty)
|
||||
}
|
||||
}
|
||||
@ -9,39 +9,6 @@ struct XSDIdentityTests {
|
||||
|
||||
private let head = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
|
||||
|
||||
@Test("a field selecting a complex-content element is rejected (cvc-identity-constraint.3)")
|
||||
func test_fieldMustBeSimpleContent() throws {
|
||||
// The field `pid` selects an element with element children (`gid`), which has
|
||||
// complex content and can never be simple-typed: invalid (idK012/idG006).
|
||||
let complexField = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence>
|
||||
<xs:element name="uid" maxOccurs="unbounded"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid"><xs:complexType><xs:sequence><xs:element name="gid"/></xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType>
|
||||
<xs:key name="k"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try !validate(complexField, "<root><uid><pid><gid>x</gid></pid></uid></root>").isEmpty)
|
||||
// Guard: a simple-content field target stays valid (no false positive).
|
||||
let simpleField = """
|
||||
\(head)
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence>
|
||||
<xs:element name="uid" maxOccurs="unbounded"><xs:complexType><xs:sequence>
|
||||
<xs:element name="pid" type="xs:string"/>
|
||||
</xs:sequence></xs:complexType></xs:element>
|
||||
</xs:sequence></xs:complexType>
|
||||
<xs:key name="k"><xs:selector xpath=".//uid"/><xs:field xpath="pid"/></xs:key>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(simpleField, "<root><uid><pid>a</pid></uid><uid><pid>b</pid></uid></root>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("xs:unique rejects a duplicated field value")
|
||||
func test_unique() throws {
|
||||
let xsd = """
|
||||
|
||||
@ -150,13 +150,13 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 20
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 23), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared
|
||||
/// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its
|
||||
/// head's type; a non-nillable element carries no xsi:nil; block/blockDefault bar substitutions (incl. an
|
||||
/// inline-typed member reached by a blocked method, disallowedSubst00105m); ur-type anyType wildcards are
|
||||
/// `lax`; identity fields compare in decimal value space, with default/fixed when absent, and must be
|
||||
/// simple-content (idK012); an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid.
|
||||
private let knownInstanceInvalidAccepted = 23
|
||||
/// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared
|
||||
/// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's
|
||||
/// type; a non-nillable element carries no xsi:nil; block/blockDefault bar substitutions (incl. an inline-
|
||||
/// typed member reached by a blocked method, disallowedSubst00105m); ur-type anyType wildcards are `lax`;
|
||||
/// identity fields compare in decimal value space (default/fixed when absent) and must be simple-content,
|
||||
/// element-children (idK012) or empty-complex (idG006); an attr wildcard is the INTERSECTION; year 0000 invalid.
|
||||
private let knownInstanceInvalidAccepted = 22
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user