fix(schema): flatten redefine self-reference and exclude prohibited uses in attribute uniqueness
The attribute-use uniqueness check (ag-props-correct.2 / ct-props-correct.4)
flattens a complex type's or attribute group's uses through attributeGroup
references. A reference to a redefined group whose redefinition self-references its
former self stopped at the cycle guard, so the original's attributes were never
reached and a duplicate they cause was missed.
Expand a redefinition's self-reference to the group's pre-redefine base definition
(already retained in the context for this purpose) so the original's attributes
join the set; a genuine non-redefine cycle still terminates without contributing.
Separately, exclude use="prohibited" attributes from the duplicate-name and
single-ID counts: a prohibited use is a removal marker, not a member of
{attribute uses} (XSD 3.4.2 / 3.6.2). This is the spec-correct reading and keeps a
self-reference that re-imports an attribute and then prohibits it from being
counted as a duplicate.
This catches W3C XSTS attQ011 and attQ017. XSTS schema invalid-accepted 22 -> 20,
valid-rejected held at 0.
This commit is contained in:
parent
64e30fe491
commit
41b3a707c3
@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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.
|
||||
- A redefinition of an `attributeGroup` that SELF-references the original and also directly re-declares one of its attributes is now rejected. The self-reference re-imports all of the original's attribute uses, so a same-named direct re-declaration produces two attribute uses of the same name, a duplicate forbidden by ag-props-correct.2. The check fires only for a re-declared, non-prohibited attribute the original directly declares, and only when both the original and the re-declaration land in no namespace (a qualified attribute's target namespace depends on cross-document `form`/chameleon resolution, so it is not compared, staying lenient); a self-referencing redefinition that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid. Catches schM8 (`c-g2` self-references and re-declares `a1`). XSTS invalid-schemas-accepted 23 → 22 (schM8), valid-schemas-rejected held at 0.
|
||||
- A redefinition of an `attributeGroup` that WIDENS a re-declared attribute's type is now rejected. A redefinition is a restriction (src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive by restriction from the original's. The check is built-in-only and therefore certain: it fires only when both the original and the re-declared attribute name a built-in type (resolved to the XSD namespace) and the re-declared built-in does not derive from the original's in the XSD derivation lattice (`int` does not derive from `boolean`). A user, inline, or absent type on either side is not compared (declines, staying lenient): an absent original type is `anySimpleType`, which any type validly restricts, a user/inline type's derivation is not resolvable here, and a user type whose local name coincides with a built-in (a `{urn:x}int`) resolves to a different namespace and is not mistaken for the built-in, so no valid restriction is rejected. Catches schM4 (`b1` re-typed `boolean` → `int`). XSTS invalid-schemas-accepted 24 → 23 (schM4), valid-schemas-rejected held at 0.
|
||||
- A redefinition of an `attributeGroup` that adds an attribute the original lacks through a NON-self `attributeGroup` reference is now rejected. A redefinition is a restriction of the original (src-redefine / cos-ct-restricts), so it may not introduce an attribute the original does not declare unless the original carries an attribute wildcard. The add-attribute check previously resolved only the redefinition's DIRECT declarations and was suppressed whenever the redefinition carried any `attributeGroup` reference, which conflated a self-reference (re-importing the original's own attributes, valid) with a non-self reference (injecting foreign attributes, invalid). The redefinition's contributed attribute names are now resolved through its non-self references and the suppression keys on a genuine self-reference (matched by namespace and local name); resolution declines, staying lenient, whenever the contributed set is not fully knowable (an unresolved group, an attribute-by-`ref`, an `anyAttribute` wildcard, or a further nested reference). This catches attgC028 (a redefinition whose `ref="car"` replaces the original's attributes with foreign ones) while keeping attgC007/attgC038/groupB018 (added attributes alongside a self-reference) valid. XSTS invalid-schemas-accepted 25 → 24 (attgC028), valid-schemas-rejected held at 0.
|
||||
|
||||
@ -7,6 +7,11 @@ private struct ReachedAttribute {
|
||||
let id: ObjectIdentifier
|
||||
let name: PureXML.Model.QualifiedName
|
||||
let isID: Bool
|
||||
/// A `use="prohibited"` attribute is a removal marker, not a member of the
|
||||
/// component's {attribute uses} (XSD §3.4.2 / §3.6.2), so it neither clashes by
|
||||
/// name nor counts toward the single-ID limit. Excluding it keeps a self-reference
|
||||
/// that re-imports an attribute and then prohibits it from reading as a duplicate.
|
||||
let isProhibited: Bool
|
||||
}
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
@ -151,15 +156,16 @@ extension PureXML.Schema.XSDParser {
|
||||
id: ObjectIdentifier(declaration),
|
||||
name: PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace),
|
||||
isID: use.type.base == .id,
|
||||
isProhibited: AttrNode.attribute(child, "use") == "prohibited",
|
||||
))
|
||||
} else if let use = attributeUse(child, context) {
|
||||
result.append(ReachedAttribute(id: ObjectIdentifier(child), name: use.name, isID: use.type.base == .id))
|
||||
result.append(ReachedAttribute(
|
||||
id: ObjectIdentifier(child), name: use.name, isID: use.type.base == .id,
|
||||
isProhibited: AttrNode.attribute(child, "use") == "prohibited",
|
||||
))
|
||||
}
|
||||
case "attributeGroup":
|
||||
guard let ref = AttrNode.attribute(child, "ref") else { break }
|
||||
let name = AttrNode.stripPrefix(ref)
|
||||
guard !visited.contains(name), let group = context.attributeGroups[name] else { break }
|
||||
result += attributeDeclarations(under: group, context, visited: visited.union([name]))
|
||||
result += referencedGroupDeclarations(child, context, visited: visited)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -167,11 +173,31 @@ extension PureXML.Schema.XSDParser {
|
||||
return result
|
||||
}
|
||||
|
||||
/// Attribute declarations reached through an `<attributeGroup ref="X">`. A first
|
||||
/// visit recurses into X's active definition. A re-visit is a redefinition's
|
||||
/// self-reference, which expands to X's pre-redefine base definition so the
|
||||
/// original's attributes join the redefined set (ag-props-correct.2, XSTS attQ011);
|
||||
/// a genuine non-redefine cycle has no base definition and contributes nothing.
|
||||
private static func referencedGroupDeclarations(
|
||||
_ child: XSDTree, _ context: PureXML.Schema.XSDContext, visited: Set<String>,
|
||||
) -> [ReachedAttribute] {
|
||||
guard let ref = AttrNode.attribute(child, "ref") else { return [] }
|
||||
let name = AttrNode.stripPrefix(ref)
|
||||
if visited.contains(name) {
|
||||
if context.redefinedAttributeGroups.contains(name), let base = context.baseAttributeGroups[name] {
|
||||
return attributeDeclarations(under: base, context, visited: visited)
|
||||
}
|
||||
return []
|
||||
}
|
||||
guard let group = context.attributeGroups[name] else { return [] }
|
||||
return attributeDeclarations(under: group, context, visited: visited.union([name]))
|
||||
}
|
||||
|
||||
private static func attributeSetErrors(_ declarations: [ReachedAttribute], _ label: String) -> [String] {
|
||||
var seenIDs: Set<ObjectIdentifier> = []
|
||||
var counts: [PureXML.Model.QualifiedName: Int] = [:]
|
||||
var idCount = 0
|
||||
for declaration in declarations where seenIDs.insert(declaration.id).inserted {
|
||||
for declaration in declarations where !declaration.isProhibited && seenIDs.insert(declaration.id).inserted {
|
||||
counts[declaration.name, default: 0] += 1
|
||||
if declaration.isID { idCount += 1 }
|
||||
}
|
||||
|
||||
@ -110,6 +110,39 @@ struct SchemaRedefineAttributeGroupTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test("a duplicate attribute via a redefined self-referencing group is rejected (attQ011)")
|
||||
func test_redefineSelfRefDuplicateAcrossReferences() throws {
|
||||
// attG references the global `foo` AND the redefined `red`; `red` self-references,
|
||||
// so its original (which declares `foo`) is re-imported. attG then holds two
|
||||
// attribute uses named `foo`: a duplicate (ag-props-correct.2).
|
||||
let main = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:redefine schemaLocation='b.xsd'><xs:attributeGroup name='red'>"
|
||||
+ "<xs:attributeGroup ref='red'/></xs:attributeGroup></xs:redefine>"
|
||||
+ "<xs:attribute name='foo' type='xs:string'/>"
|
||||
+ "<xs:attributeGroup name='attG'><xs:attribute ref='foo'/><xs:attributeGroup ref='red'/></xs:attributeGroup>"
|
||||
+ "<xs:complexType name='ct'><xs:attributeGroup ref='attG'/></xs:complexType>"
|
||||
+ "<xs:element name='e' type='ct'/></xs:schema>"
|
||||
let baseDoc = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:attributeGroup name='red'><xs:attribute name='foo' type='xs:string'/></xs:attributeGroup></xs:schema>"
|
||||
#expect(throws: (any Error).self) { try PureXML.Schema.Document(main, schemaLoader: { _ in baseDoc }) }
|
||||
}
|
||||
|
||||
@Test("a redefined self-referencing group supplying a distinct attribute is valid (no false duplicate)")
|
||||
func test_redefineSelfRefDistinctAttributeAccepted() throws {
|
||||
// Same shape, but `red`'s original declares `bar`, not `foo`: attG holds `foo`
|
||||
// and `bar`, no duplicate, so the self-reference expansion must not over-flag.
|
||||
let main = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:redefine schemaLocation='b.xsd'><xs:attributeGroup name='red'>"
|
||||
+ "<xs:attributeGroup ref='red'/></xs:attributeGroup></xs:redefine>"
|
||||
+ "<xs:attribute name='foo' type='xs:string'/>"
|
||||
+ "<xs:attributeGroup name='attG'><xs:attribute ref='foo'/><xs:attributeGroup ref='red'/></xs:attributeGroup>"
|
||||
+ "<xs:complexType name='ct'><xs:attributeGroup ref='attG'/></xs:complexType>"
|
||||
+ "<xs:element name='e' type='ct'/></xs:schema>"
|
||||
let baseDoc = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
+ "<xs:attributeGroup name='red'><xs:attribute name='bar' type='xs:string'/></xs:attributeGroup></xs:schema>"
|
||||
_ = try PureXML.Schema.Document(main, schemaLoader: { _ in baseDoc })
|
||||
}
|
||||
|
||||
@Test("widening a re-declared attribute's built-in type is rejected (schM4)")
|
||||
func test_widenBuiltinTypeRejected() throws {
|
||||
// int does not derive by restriction from boolean, so re-typing the attribute
|
||||
|
||||
@ -146,9 +146,9 @@ struct XSTSSuiteTests {
|
||||
/// model-group occurrence, identity-constraint content, derivation base kind, attribute/simple-type,
|
||||
/// composition, simpleContent inline-type, complexContent mixedness, and substitution-group anySimpleType checks.
|
||||
/// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35.
|
||||
/// UPA `##other` 31; ext-model 29; XSD-ns-ref 27; unprefixed-type-ref 25; redefine non-self-add attgC028 24; type-widen schM4 23; self-ref dup schM8 22; valid-rejected 0.
|
||||
/// UPA 31; ext-model 29; XSD-ns 27; unpref-type-ref 25; redefine-attrGroup attgC028/schM4/schM8 25->22, attQ011/017 ref-dup 22->20; valid-rejected 0.
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 22
|
||||
private let knownSchemaInvalidAccepted = 20
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in
|
||||
|
||||
Loading…
Reference in New Issue
Block a user