fix(schema): reject two ID-typed attributes on one element at instance time; invalid-instances-accepted 45 to 43 (#147)
XSD 1.0 §3.4.6 (cvc-complex-type) allows an element at most one attribute whose type is xs:ID. A complex type that DECLARES two ID-typed attribute uses is already a compile-time schema error, but two ID-typed attributes can also reach an element through an xs:anyAttribute wildcard that admits two ID-typed global attributes (attZ014), which was accepted. validateAttributes now counts the present attributes whose effective type is xs:ID, taking the type from the declared use an attribute matches or the global declaration a matching wildcard admits (so a restriction of xs:ID counts), and reports an error when more than one is present. xs:IDREF/xs:ENTITY and a single ID attribute are unaffected, and a valid instance never carries two, so the check cannot over-reject. invalid-instances-accepted 45 to 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
This commit is contained in:
parent
ca56caf11c
commit
91894d5759
@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is `xs:ID` (XSD 1.0 §3.4.6). Two ID-typed attributes admitted through an `xs:anyAttribute` wildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction of `xs:ID`; `xs:IDREF`/`xs:ENTITY` and a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#146, #147): an identity-constraint (`unique`/`key`/`keyref`) field that selects an absent attribute or an empty element whose declaration carries a `default`/`fixed` now takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed). The schema's `default`/`fixed` value constraints on identity-field targets are gathered at compile time, keyed the same way as the field types, and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an `@attr` field is defaulted only when the attribute is ABSENT (empty node-set), never when it is present with value `""`; a `.` element field is defaulted only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. invalid-instances-accepted 50 → 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#147): a `block` on a substitution-group head's TYPE, not only on the head element, now bars a member from substituting. The substitution-group filter drops a member whose type reaches the head's type by a blocked derivation method, using the union of the head element's `{disallowed substitutions}` and the head type's `{prohibited substitutions}`; previously only the head element's own `block` was consulted, so a `block="restriction"` on the head's complex type let a restriction-derived member substitute. The element-level `block="substitution"` outright bar is unchanged, and a member derived by a method the block does not list still substitutes. invalid-instances-accepted 53 → 50, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
@ -21,6 +21,42 @@ extension PureXML.Schema.ComplexValidator {
|
||||
}
|
||||
}
|
||||
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
||||
if idTypedAttributeCount(present, type, on: element) > 1 {
|
||||
errors.append(PureXML.Validation.ValidationError(
|
||||
reason: "an element may carry at most one attribute of type ID",
|
||||
at: path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of `present` attributes whose effective type is `xs:ID` (an
|
||||
/// element may carry at most one; XSD 1.0 §3.4.6, cvc-complex-type). An
|
||||
/// attribute's type is its declared use's type when it matches one, otherwise
|
||||
/// the global declaration a matching wildcard admits.
|
||||
private func idTypedAttributeCount(
|
||||
_ present: [PureXML.Model.Attribute],
|
||||
_ type: PureXML.Schema.ComplexType,
|
||||
on element: PureXML.Model.Element,
|
||||
) -> Int {
|
||||
present.reduce(into: 0) { count, attribute in
|
||||
if isIDTyped(attribute, type, on: element) { count += 1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `attribute`'s effective type is `xs:ID`: the type of the declared
|
||||
/// use it matches, or the global declaration a matching wildcard admits.
|
||||
private func isIDTyped(
|
||||
_ attribute: PureXML.Model.Attribute,
|
||||
_ type: PureXML.Schema.ComplexType,
|
||||
on element: PureXML.Model.Element,
|
||||
) -> Bool {
|
||||
if let use = type.attributes.first(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: element.name) }) {
|
||||
return use.type.isID
|
||||
}
|
||||
guard let wildcard = type.attributeWildcard, wildcard.admits(attribute.name),
|
||||
let use = globalAttributeUse(for: attribute.name)
|
||||
else { return false }
|
||||
return use.type.isID
|
||||
}
|
||||
|
||||
/// Whether an instance attribute satisfies a declared use. Chameleon-included
|
||||
|
||||
39
Tests/SchemaTwoIDAttributesTests.swift
Normal file
39
Tests/SchemaTwoIDAttributesTests.swift
Normal file
@ -0,0 +1,39 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// An element may carry at most one attribute whose type is `xs:ID` (XSD 1.0
|
||||
/// §3.4.6, cvc-complex-type). Two ID-typed attributes on one element, whether
|
||||
/// declared or admitted through an attribute wildcard, are invalid; one is fine.
|
||||
@Suite("at most one ID attribute per element")
|
||||
struct SchemaTwoIDAttributesTests {
|
||||
/// The W3C attZ014 shape: the two ID-typed attributes are not declared on the
|
||||
/// type (which would already be a schema error) but enter the instance through
|
||||
/// an `anyAttribute` wildcard that admits two ID-typed global attributes, one
|
||||
/// of them a restriction of `xs:ID`.
|
||||
@Test("two ID-typed attributes admitted via a wildcard are rejected")
|
||||
func test_twoWildcardIDsRejected() throws {
|
||||
let schema = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:simpleType name="idSub"><xs:restriction base="xs:ID"/></xs:simpleType>
|
||||
<xs:attribute name="a" type="xs:ID"/>
|
||||
<xs:attribute name="b" type="idSub"/>
|
||||
<xs:element name="e"><xs:complexType><xs:anyAttribute processContents="strict"/></xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try !schema.validate(#"<e a="i1" b="i2"/>"#).isEmpty)
|
||||
}
|
||||
|
||||
@Test("a single ID attribute plus non-ID attributes is accepted")
|
||||
func test_singleIDAccepted() throws {
|
||||
let schema = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="e"><xs:complexType>
|
||||
<xs:attribute name="id" type="xs:ID"/>
|
||||
<xs:attribute name="ref" type="xs:IDREF"/>
|
||||
<xs:attribute name="s" type="xs:string"/>
|
||||
</xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try schema.validate(#"<e id="i1" ref="i1" s="x"/>"#).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -150,13 +150,13 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 69
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 45), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// Instance bucket (133 -> 43), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex, then atomic and list/union, resolved through the element-ref chain);
|
||||
/// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the
|
||||
/// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`;
|
||||
/// identity fields compare in the decimal value space and take a declared default/fixed when
|
||||
/// absent or empty; a complex type's attribute wildcard is the INTERSECTION of its sources.
|
||||
private let knownInstanceInvalidAccepted = 45
|
||||
private let knownInstanceInvalidAccepted = 43
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user