diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60db601..5587af1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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 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.
- A schema reference (`ref`/`substitutionGroup`) that resolves to the XSD namespace is now rejected as undeclared. The XSD namespace declares no referenceable user component, so `isUndeclaredReferenceType` already rejected a `type`/`base` in it, but `isUndeclaredReferenceRef` fell through to a lenient accept. This is reached when an unprefixed reference picks up a default `xmlns` of the XSD namespace, so it is not in the target namespace where the schema's own components live: e.g. `xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"` with `` resolves to `{XSD}root`, not `{foo}root`, and is undeclared. A schema that itself targets the XSD namespace, and a genuinely loaded XSD-namespace document, are unaffected. XSTS invalid-schemas-accepted 29 → 27 (xsd013/xsd014), valid-schemas-rejected held at 0.
diff --git a/Sources/Schema/SchemaRedefineSelfReference.swift b/Sources/Schema/SchemaRedefineSelfReference.swift
index c11d105..0778458 100644
--- a/Sources/Schema/SchemaRedefineSelfReference.swift
+++ b/Sources/Schema/SchemaRedefineSelfReference.swift
@@ -194,6 +194,9 @@ extension PureXML.Schema.XSDParser {
if relaxesFixedValue(base: attribute, redefined: redefined[attrName], use: use(attrName)) {
errors.append("a redefined attribute group '\(name)' may not relax the fixed value of '\(attrName)'")
}
+ 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)'")
@@ -230,6 +233,37 @@ extension PureXML.Schema.XSDParser {
.map { "a redefined attribute group '\(name)' may not add the attribute '\($0)'" }
}
+ /// 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
+ /// when both the original and the re-declared attribute name a built-in type and the
+ /// re-declared built-in does not derive from the original's in the XSD derivation
+ /// lattice (e.g. `int` does not derive from `boolean`, XSTS schM4). A user, inline,
+ /// or absent type on either side is not compared (returns false, staying lenient):
+ /// an absent original type is `anySimpleType`, which any type validly restricts, and
+ /// a user/inline type's derivation is not resolvable here, so no valid restriction
+ /// is rejected.
+ private static func widensBuiltinType(base: XSDTree, redefined: XSDTree?) -> Bool {
+ guard let redefined,
+ let baseBuiltin = builtinType(of: base),
+ let redefinedBuiltin = builtinType(of: redefined)
+ else { return false }
+ return !redefinedBuiltin.derives(from: baseBuiltin)
+ }
+
+ /// The built-in type an attribute names, ONLY when its `type` QName resolves to the
+ /// XSD namespace. A user type whose local name coincides with a built-in (a
+ /// `{urn:x}int`) resolves to a different namespace and returns nil, so it is not
+ /// mistaken for the built-in and `widensBuiltinType` declines rather than reject a
+ /// valid restriction. The QName is resolved against the owning schema's bindings,
+ /// the same context `redefineSelfReferenceFindings` uses for redefine references.
+ private static func builtinType(of attribute: XSDTree) -> PureXML.Schema.BuiltinType? {
+ guard let type = RedefineNode.attribute(attribute, "type") else { return nil }
+ let bindings = RedefineNode.namespaceBindings(of: RedefineNode.schemaOwner(attribute))
+ guard RedefineNode.referenceNamespace(type, bindings) == xsdNamespace else { return nil }
+ return PureXML.Schema.BuiltinType(rawValue: RedefineNode.stripPrefix(type))
+ }
+
/// A base attribute with a `fixed` value is relaxed when the redefinition declares
/// a matching, non-prohibited attribute whose `fixed` value differs (or is absent).
/// Prohibiting or omitting the attribute is not relaxation, so it is left alone.
diff --git a/Tests/SchemaRedefineAttributeGroupTests.swift b/Tests/SchemaRedefineAttributeGroupTests.swift
index c11ccc5..02800f0 100644
--- a/Tests/SchemaRedefineAttributeGroupTests.swift
+++ b/Tests/SchemaRedefineAttributeGroupTests.swift
@@ -88,6 +88,52 @@ struct SchemaRedefineAttributeGroupTests {
)
}
+ @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
+ // from boolean to int widens it: a redefinition must restrict, not widen.
+ #expect(throws: (any Error).self) {
+ try redefine(
+ group: "",
+ base: "",
+ )
+ }
+ }
+
+ @Test("narrowing a re-declared attribute's built-in type within the lattice is valid")
+ func test_narrowBuiltinTypeAccepted() throws {
+ // int derives from integer, so re-typing integer -> int is a valid restriction.
+ _ = try redefine(
+ group: "",
+ base: "",
+ )
+ }
+
+ @Test("re-typing an originally untyped attribute is valid (anySimpleType base)")
+ func test_retypeUntypedBaseAccepted() throws {
+ // An absent base type is anySimpleType, which any type validly restricts.
+ _ = try redefine(
+ group: "",
+ base: "",
+ )
+ }
+
+ @Test("a user type whose local name coincides with a built-in is not mistaken for it (no false positive)")
+ func test_userTypeNamedLikeBuiltinNotWidened() throws {
+ // The redefinition re-types `a` to `tns:int`, a USER simpleType that restricts
+ // xs:string. Its local name is the built-in keyword `int`, but it resolves to
+ // the target namespace, not the XSD namespace, so the type-widening check must
+ // not treat it as xsd:int (which would not derive from xsd:string) and reject.
+ let main = ""
+ + ""
+ + ""
+ + ""
+ + ""
+ let baseDoc = ""
+ + ""
+ _ = try PureXML.Schema.Document(main, schemaLoader: { _ in baseDoc })
+ }
+
@Test("a non-self reference injecting a foreign attribute is rejected (attgC028)")
func test_nonSelfReferenceAddRejected() throws {
let main = ""
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 57dc060..71af5c9 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -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` 35->31; UPA ext-model 31->29; ref-to-XSD-ns 29->27; unprefixed type-ref 27->25; redefine attrGroup non-self-ref add (attgC028) 25->24; valid-rejected 0.
+ /// 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.
private let knownSchemaValidRejected = 0
- private let knownSchemaInvalidAccepted = 24
+ private let knownSchemaInvalidAccepted = 23
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