fix(schema): reject redefined attribute group that self-references and re-declares an attribute
A redefinition of an attributeGroup that self-references the original re-imports all of its attribute uses, so directly re-declaring one of those attributes produces two attribute uses of the same name, a duplicate forbidden by ag-props-correct.2. Flag a non-prohibited direct re-declaration of an attribute the original directly declares, when the redefinition has a self-reference. To stay false-positive-free the check fires 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); a self-referencing redefinition that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid. Also extracts the use-transition restriction errors into a helper to keep the function within the complexity limit. This catches W3C XSTS schM8. XSTS schema invalid-accepted 23 -> 22, valid-rejected held at 0.
This commit is contained in:
parent
b6e5cd8f3f
commit
64e30fe491
@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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.
|
||||
- An unprefixed `type`/`base`/`itemType`/`memberTypes` reference to a type declared in a non-empty target namespace is now rejected as undeclared. With no default `xmlns`, an unprefixed QName resolves to the absent namespace, so `type="t"` is `{}t`, not `{urn:x}t` where the type lives; `isUndeclaredReferenceType` matched the type by local name and accepted it. It now mirrors `isUndeclaredReferenceRef`: a no-namespace reference is undeclared when the schema has a non-empty target namespace (a chameleon-included global, and an imported no-namespace type via the foreign pools, remain accepted). XSTS invalid-schemas-accepted 27 → 25 (addB009/test66059 and one more), valid-schemas-rejected held at 0.
|
||||
|
||||
@ -113,15 +113,12 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
|
||||
/// The non-prohibited attribute names a redefinition pulls in through its
|
||||
/// `attributeGroup` references, with whether any of them is a SELF-reference
|
||||
/// (resolving by NAMESPACE and local name to the redefinition's own name+target).
|
||||
/// A self-reference re-imports the original and is reported via `hasSelfReference`
|
||||
/// rather than counted. Returns nil (decline, stay lenient) when a referenced
|
||||
/// group does not resolve from the base pool, carries an attribute wildcard, nests
|
||||
/// a further `attributeGroup` reference, or names an attribute by `ref` (not
|
||||
/// `name`): in each case the contributed set is not fully known, so the add-check
|
||||
/// must not fire. A redefinition with no `attributeGroup` reference yields an empty
|
||||
/// set and no self-reference, preserving the direct-declaration-only behavior.
|
||||
/// `attributeGroup` references, with whether any is a SELF-reference (resolving by
|
||||
/// NAMESPACE and local name to the redefinition's own name+target; reported via
|
||||
/// `hasSelfReference` rather than counted, since it re-imports the original). Returns
|
||||
/// nil (decline, stay lenient) when the contributed set is not fully known: a
|
||||
/// referenced group that does not resolve, carries an attribute wildcard, nests a
|
||||
/// further `attributeGroup` reference, or names an attribute by `ref` (not `name`).
|
||||
private static func referencedAttributeNames(
|
||||
of redefinition: XSDTree, ownName: String, base: [String: XSDTree],
|
||||
) -> (names: Set<String>, hasSelfReference: Bool)? {
|
||||
@ -197,13 +194,16 @@ extension PureXML.Schema.XSDParser {
|
||||
if use(attrName) != "prohibited", widensBuiltinType(base: attribute, redefined: redefined[attrName]) {
|
||||
errors.append("a redefined attribute group '\(name)' may not widen the type of '\(attrName)'")
|
||||
}
|
||||
switch RedefineNode.attribute(attribute, "use") {
|
||||
case "required" where !hasReference && redefined[attrName] == nil:
|
||||
errors.append("a redefined attribute group '\(name)' may not eliminate the required attribute '\(attrName)'")
|
||||
case "prohibited" where !baseHasWildcard && use(attrName) != nil && use(attrName) != "prohibited":
|
||||
errors.append("a redefined attribute group '\(name)' may not re-introduce the prohibited attribute '\(attrName)'")
|
||||
default:
|
||||
break
|
||||
if redeclaresSelfSuppliedAttribute(
|
||||
attribute,
|
||||
redeclaration: redefined[attrName],
|
||||
hasSelfReference: referenced?.hasSelfReference == true,
|
||||
prohibited: use(attrName) == "prohibited",
|
||||
) {
|
||||
errors.append("a redefined attribute group '\(name)' redeclares '\(attrName)', which its self-reference already supplies")
|
||||
}
|
||||
if let transition = useTransitionError(base: attribute, redefined: redefined[attrName], name: name, hasReference: hasReference, baseHasWildcard: baseHasWildcard) {
|
||||
errors.append(transition)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
@ -233,6 +233,49 @@ extension PureXML.Schema.XSDParser {
|
||||
.map { "a redefined attribute group '\(name)' may not add the attribute '\($0)'" }
|
||||
}
|
||||
|
||||
/// The `use`-transition restriction errors for one base attribute: a redefinition
|
||||
/// may not eliminate a REQUIRED attribute (unless a reference could re-supply it),
|
||||
/// nor re-introduce a PROHIBITED attribute as usable (unless the original has an
|
||||
/// attribute wildcard that could admit it).
|
||||
private static func useTransitionError(
|
||||
base: XSDTree, redefined: XSDTree?, name: String, hasReference: Bool, baseHasWildcard: Bool,
|
||||
) -> String? {
|
||||
guard let attrName = RedefineNode.attribute(base, "name") else { return nil }
|
||||
let use = redefined.map { RedefineNode.attribute($0, "use") ?? "optional" }
|
||||
switch RedefineNode.attribute(base, "use") {
|
||||
case "required" where !hasReference && redefined == nil:
|
||||
return "a redefined attribute group '\(name)' may not eliminate the required attribute '\(attrName)'"
|
||||
case "prohibited" where !baseHasWildcard && use != nil && use != "prohibited":
|
||||
return "a redefined attribute group '\(name)' may not re-introduce the prohibited attribute '\(attrName)'"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// schM8 / ag-props-correct.2: when a redefinition SELF-references the original it
|
||||
/// re-imports all of the original's attributes, so directly re-declaring one of them
|
||||
/// produces two attribute uses of the same name (a duplicate), which is invalid. 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).
|
||||
private static func redeclaresSelfSuppliedAttribute(
|
||||
_ attribute: XSDTree, redeclaration: XSDTree?, hasSelfReference: Bool, prohibited: Bool,
|
||||
) -> Bool {
|
||||
guard hasSelfReference, let redeclaration, !prohibited else { return false }
|
||||
return isNoNamespaceAttribute(attribute) && isNoNamespaceAttribute(redeclaration)
|
||||
}
|
||||
|
||||
/// Whether an attribute use lands in NO namespace: it is not `form="qualified"` and
|
||||
/// its owning schema does not set `attributeFormDefault="qualified"`. A qualified
|
||||
/// attribute's target namespace depends on cross-document `form`/chameleon
|
||||
/// resolution, so the self-reference duplicate check (which compares by local name)
|
||||
/// is sound only for no-namespace attributes; a qualified one is not compared.
|
||||
private static func isNoNamespaceAttribute(_ attribute: XSDTree) -> Bool {
|
||||
if RedefineNode.attribute(attribute, "form") == "qualified" { return false }
|
||||
return RedefineNode.attribute(RedefineNode.schemaOwner(attribute), "attributeFormDefault") != "qualified"
|
||||
}
|
||||
|
||||
/// Whether a redefined attribute WIDENS the original's type. A redefinition is a
|
||||
/// restriction, 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
|
||||
|
||||
@ -80,6 +80,28 @@ struct SchemaRedefineAttributeGroupTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test("re-declaring an attribute the self-reference already supplies is rejected (schM8)")
|
||||
func test_selfRefRedeclareDuplicateRejected() throws {
|
||||
// The self-reference re-imports the original's `a`, so directly re-declaring `a`
|
||||
// produces two attribute uses of the same name: a duplicate (ag-props-correct.2).
|
||||
#expect(throws: (any Error).self) {
|
||||
try redefine(
|
||||
group: "<xs:attributeGroup ref='g'/><xs:attribute name='a' type='xs:string'/>",
|
||||
base: "<xs:attribute name='a' type='xs:string'/>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("a self-reference re-declaring a prohibited attribute is not flagged as duplicate (lenient)")
|
||||
func test_selfRefRedeclareProhibitedLenient() throws {
|
||||
// A `use="prohibited"` re-declaration removes rather than duplicates, so the
|
||||
// duplicate check declines; the base attribute is optional, so this compiles.
|
||||
_ = try redefine(
|
||||
group: "<xs:attributeGroup ref='g'/><xs:attribute name='a' use='prohibited' type='xs:string'/>",
|
||||
base: "<xs:attribute name='a' type='xs:string'/>",
|
||||
)
|
||||
}
|
||||
|
||||
@Test("adding an attribute alongside a self-reference is valid")
|
||||
func test_addWithSelfReferenceAccepted() throws {
|
||||
_ = try redefine(
|
||||
|
||||
@ -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; redefine type-widen schM4 23; valid-rejected 0.
|
||||
/// 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.
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 23
|
||||
private let knownSchemaInvalidAccepted = 22
|
||||
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