diff --git a/CHANGELOG.md b/CHANGELOG.md index 31356ba..9dd3bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- XSTS invalid-instances-accepted (#147): a `simpleContent` derivation whose base is another complex type with simpleContent now resolves through the chain to the underlying simple type, so the element's text is validated against it. A type restricting or extending a complex type that ultimately derives from a built-in (e.g. `Test` restricts `Test2` which extends `xs:int`) validates its content as that built-in rather than as unconstrained string; `baseTD00101`'s non-int text is now rejected. The base resolution recurses through `complexTypeNodes`, accumulating facets, with a cycle guard. invalid-instances-accepted 39 → 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. + - XSTS invalid-instances-accepted (#147): `use="prohibited"` on an attribute now excludes it from the complex type's effective `{attribute uses}` (XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard's `processContents`) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. - 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. diff --git a/Sources/Schema/XSDParser.swift b/Sources/Schema/XSDParser.swift index 5d1e942..21d0cfe 100644 --- a/Sources/Schema/XSDParser.swift +++ b/Sources/Schema/XSDParser.swift @@ -367,24 +367,34 @@ extension PureXML.Schema.XSDParser { return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: mixed ? .mixed(particle) : .elementOnly(particle)) } - private static func simpleContentType(_ node: XSDTree, _ context: XSDContext) -> SimpleType { + private static func simpleContentType(_ node: XSDTree, _ context: XSDContext, visited: Set = []) -> SimpleType { guard let inner = derivation(node) else { return SimpleType(base: .string) } let rawBase = XSDNode.attribute(inner, "base") ?? "string" let baseName = XSDNode.stripPrefix(rawBase) let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: inner, defaultBindings: context.namespaceBindings) let uri = XSDNode.referenceNamespace(rawBase, bindings) - let base: BuiltinType - let inheritedFacets: Facets - if uri == PureXML.Schema.XSDParser.xsdNamespace { - base = BuiltinType(rawValue: baseName) ?? .string - inheritedFacets = Facets() - } else { - base = context.simpleTypes[baseName]?.base ?? .string - inheritedFacets = context.simpleTypes[baseName]?.facets ?? Facets() - } - var facets = inheritedFacets + let baseType = simpleContentBaseType(baseName, uri: uri, context, visited: visited) + var facets = baseType.facets XSDSimpleParser.applyFacets(inner, into: &facets) - return SimpleType(base: base, facets: facets) + return SimpleType(base: baseType.base, facets: facets) + } + + /// The simple type a `simpleContent` derivation derives from: an XSD built-in, a + /// named simple type, or, when the base is another complex type with simpleContent, + /// that type's own resolved simple type (so `Test` restricting `Test2` extending + /// `xs:int` resolves to `int`). A cycle in the base chain falls back to `string`. + private static func simpleContentBaseType(_ baseName: String, uri: String?, _ context: XSDContext, visited: Set) -> SimpleType { + if uri == PureXML.Schema.XSDParser.xsdNamespace { + return SimpleType(base: BuiltinType(rawValue: baseName) ?? .string) + } + if let simple = context.simpleTypes[baseName] { + return simple + } + guard !visited.contains(baseName), + let complexNode = context.complexTypeNodes[baseName], + let baseSimpleContent = XSDNode.firstChild(complexNode, named: "simpleContent") + else { return SimpleType(base: .string) } + return simpleContentType(baseSimpleContent, context, visited: visited.union([baseName])) } } diff --git a/Tests/SchemaSimpleContentChainTests.swift b/Tests/SchemaSimpleContentChainTests.swift new file mode 100644 index 0000000..5b3a607 --- /dev/null +++ b/Tests/SchemaSimpleContentChainTests.swift @@ -0,0 +1,38 @@ +@testable import PureXML +import Testing + +/// A `simpleContent` derivation whose base is another complex type with +/// simpleContent resolves through the chain to the underlying simple type, so the +/// element's text is validated against it. A type restricting (or extending) a +/// complex type that ultimately derives from `xs:int` validates its content as an +/// int, not as unconstrained string (W3C sun baseTD00101). +@Suite("simpleContent base chain") +struct SchemaSimpleContentChainTests { + private func schema(method: String) throws -> PureXML.Schema.Document { + try PureXML.Schema.Document(""" + + + + + + + + + + """) + } + + @Test("text invalid for the chained int base is rejected, valid int accepted") + func test_restrictionChainValidatesAsInt() throws { + let restriction = try schema(method: "restriction") + #expect(try !restriction.validate(#"b"#).isEmpty) + #expect(try restriction.validate(#"42"#).isEmpty) + } + + @Test("an extension chain to int also validates the content as int") + func test_extensionChainValidatesAsInt() throws { + let extension0 = try schema(method: "extension") + #expect(try !extension0.validate(#"b"#).isEmpty) + #expect(try extension0.validate(#"42"#).isEmpty) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 6027988..e12428d 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 69 private let knownInstanceValidRejected = 0 - /// Instance bucket (133 -> 39), per-step deltas in CHANGELOG.md: xsi:type must derive from the + /// Instance bucket (133 -> 37), 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 = 39 + private let knownInstanceInvalidAccepted = 37 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {