From 380160cc2b0fb107595978fe3e4a266193adf070 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 12 Jul 2026 14:12:17 +0200 Subject: [PATCH] refactor(validation): decompose XSD instance validation into composable rules The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass. --- PublicAPITests/PublicSurfaceTests.swift | 4 +- Sources/Canonical/CanonicalNodeSubset.swift | 16 +- .../Canonical/CanonicalPrefixRewrite.swift | 8 +- Sources/Canonical/CanonicalURI.swift | 42 ++- Sources/Canonical/Canonicalizer.swift | 32 +- Sources/Catalog/CatalogParser.swift | 8 +- Sources/Catalog/Resolver.swift | 12 +- Sources/Catalog/SGMLCatalogParser.swift | 24 +- Sources/Emitting/Escaping.swift | 36 ++- Sources/HTML/HTMLAdoptionAgency.swift | 40 ++- Sources/HTML/HTMLDocumentBuilder.swift | 58 +++- Sources/HTML/HTMLEntities.swift | 8 +- Sources/HTML/HTMLSerializer.swift | 12 +- Sources/HTML/HTMLTokenizer.swift | 39 ++- Sources/Model/Node.swift | 4 +- Sources/Model/TreeNode.swift | 8 +- Sources/Model/TreeNodeConversion.swift | 4 +- Sources/Model/TreeNodeEditing.swift | 12 +- Sources/Model/TreeNodeMutation.swift | 4 +- Sources/Model/TreeNodeSpan.swift | 4 +- Sources/Parsing/Big5Decoder.swift | 8 +- Sources/Parsing/ByteDecoder.swift | 8 +- Sources/Parsing/ByteEncoder.swift | 8 +- Sources/Parsing/CJKByteEncoder.swift | 4 +- Sources/Parsing/DTDAttListGrammar.swift | 12 +- Sources/Parsing/DTDContentModelGrammar.swift | 12 +- Sources/Parsing/DoctypeScanner.swift | 4 +- .../Parsing/DoctypeScannerConditional.swift | 8 +- Sources/Parsing/DoctypeScannerStrict.swift | 8 +- Sources/Parsing/EUCJPDecoder.swift | 8 +- Sources/Parsing/EUCKRDecoder.swift | 8 +- Sources/Parsing/EntityDecoder.swift | 4 +- .../Parsing/EntityReplacementGrammar.swift | 40 ++- Sources/Parsing/EventReaderContent.swift | 16 +- Sources/Parsing/EventReaderRecovery.swift | 12 +- Sources/Parsing/EventReaderValidity.swift | 16 +- Sources/Parsing/GB18030Decoder.swift | 8 +- Sources/Parsing/GBKDecoder.swift | 8 +- Sources/Parsing/ISO2022JPDecoder.swift | 4 +- Sources/Parsing/PseudoAttributeScanner.swift | 4 +- Sources/Parsing/PushParser.swift | 8 +- Sources/Parsing/PushScanner.swift | 32 +- Sources/Parsing/Reader.swift | 40 ++- Sources/Parsing/ReaderByteRuns.swift | 12 +- Sources/Parsing/ShiftJISDecoder.swift | 20 +- Sources/Parsing/StreamingDecoder.swift | 36 ++- Sources/Parsing/StreamingDecoderEUCTW.swift | 4 +- Sources/Parsing/TextReader.swift | 4 +- Sources/Parsing/XMLCharacter.swift | 8 +- Sources/Pattern/Matcher.swift | 8 +- Sources/Pattern/PatternCompiler.swift | 24 +- Sources/Pattern/PatternStream.swift | 4 +- Sources/Regex/RegexAutomaton.swift | 14 +- Sources/Regex/RegexCategory.swift | 4 +- Sources/Regex/RegexParser.swift | 32 +- Sources/Regex/RegexSyntax.swift | 4 +- Sources/Regex/XSDCategory.swift | 4 +- Sources/Schema/BuiltinType.swift | 12 +- Sources/Schema/ComplexType.swift | 32 +- Sources/Schema/ComplexValidator.swift | 184 ++++------- .../Schema/ComplexValidatorAttributes.swift | 32 +- .../Schema/ComplexValidatorContentModel.swift | 24 +- .../Schema/ComplexValidatorDerivation.swift | 48 ++- Sources/Schema/ComplexValidatorHelpers.swift | 24 +- Sources/Schema/ComplexValidatorNilFixed.swift | 62 ++++ .../Schema/ComplexValidatorStreaming.swift | 72 ++++- .../Schema/ComplexValidatorWildcards.swift | 48 +++ Sources/Schema/ComplexValidatorXSIType.swift | 18 +- Sources/Schema/ContentMatcher.swift | 8 +- Sources/Schema/ContentModelDeterminism.swift | 8 +- ...ContentModelDeterminismCompositional.swift | 32 +- .../ContentModelDeterminismConflict.swift | 12 +- .../Schema/ContentModelEffectiveModel.swift | 8 +- .../Schema/ContentModelGroupDependency.swift | 4 +- Sources/Schema/ContentNFABuilder.swift | 6 +- Sources/Schema/ContentRTN.swift | 10 +- Sources/Schema/ContentRTNMatcher.swift | 28 +- Sources/Schema/DateTimeParser.swift | 15 +- Sources/Schema/DecimalValue.swift | 12 +- Sources/Schema/DurationValue.swift | 12 +- Sources/Schema/IDTracker.swift | 12 +- Sources/Schema/IdentityFieldSimplicity.swift | 8 +- Sources/Schema/IdentityFieldType.swift | 4 +- Sources/Schema/IdentityTupleSet.swift | 4 +- Sources/Schema/IdentityValidator.swift | 126 +++----- .../Schema/IdentityValidatorDefaults.swift | 8 +- Sources/Schema/IdentityValidatorHelpers.swift | 71 ++++ Sources/Schema/Lexical.swift | 16 +- Sources/Schema/OccurrenceRange.swift | 16 +- Sources/Schema/ParticleNormalization.swift | 4 +- Sources/Schema/ParticleRestriction.swift | 20 +- .../Schema/ParticleRestrictionElement.swift | 28 +- Sources/Schema/RelaxNGCompactHelpers.swift | 14 +- Sources/Schema/RelaxNGCompactParser.swift | 56 +++- Sources/Schema/RelaxNGDerivative.swift | 83 +++-- Sources/Schema/RelaxNGDiagnostics.swift | 12 +- Sources/Schema/RelaxNGGrammarCheck.swift | 24 +- .../Schema/RelaxNGGrammarCheckSections.swift | 16 +- Sources/Schema/RelaxNGNode.swift | 12 +- Sources/Schema/RelaxNGParser.swift | 8 +- .../Schema/RelaxNGRestrictionChecker.swift | 40 ++- .../Schema/RelaxNGRestrictionOverlap.swift | 8 +- Sources/Schema/RelaxNGRestrictionPaths.swift | 48 ++- Sources/Schema/RelaxNGRestrictions.swift | 4 +- Sources/Schema/RelaxNGSchema.swift | 4 +- Sources/Schema/SchemaAllGroupPlacement.swift | 4 +- .../Schema/SchemaAttributeRestriction.swift | 8 +- .../Schema/SchemaAttributeUniqueness.swift | 4 +- Sources/Schema/SchemaContentDerivation.swift | 24 +- Sources/Schema/SchemaDerivationControl.swift | 12 +- Sources/Schema/SchemaDocument.swift | 33 +- Sources/Schema/SchemaFacetApplicability.swift | 32 +- Sources/Schema/SchemaIDAttribute.swift | 4 +- Sources/Schema/SchemaIDValueConstraint.swift | 4 +- Sources/Schema/SchemaIdentityXPath.swift | 4 +- Sources/Schema/SchemaNameUniqueness.swift | 28 +- Sources/Schema/SchemaNestedDefinition.swift | 4 +- Sources/Schema/SchemaNotationValidity.swift | 4 +- ...emaRedefineAttributeGroupRestriction.swift | 12 +- .../Schema/SchemaRedefineSelfReference.swift | 16 +- .../Schema/SchemaReferenceForeignPools.swift | 4 +- .../Schema/SchemaReferenceResolution.swift | 16 +- Sources/Schema/SchemaReferences.swift | 16 +- Sources/Schema/SchemaSimpleTypeFinal.swift | 4 +- Sources/Schema/SchemaSimpleTypeValidity.swift | 8 +- Sources/Schema/SchemaSimpleTypeVariety.swift | 16 +- Sources/Schema/SchemaStructure.swift | 4 +- Sources/Schema/SchemaStructureHelpers.swift | 28 +- Sources/Schema/SchemaSubstitutionType.swift | 20 +- .../Schema/SchemaUndeclaredNamespace.swift | 8 +- Sources/Schema/SchemaWildcardNamespace.swift | 4 +- Sources/Schema/SchemaXSIAttribute.swift | 4 +- Sources/Schema/SimpleType.swift | 72 +++-- Sources/Schema/StepSiblingIndex.swift | 4 +- Sources/Schema/StreamingXSDValidator.swift | 4 +- Sources/Schema/XPathQueryCache.swift | 4 +- Sources/Schema/XSDAttributeParser.swift | 16 +- Sources/Schema/XSDCompletions.swift | 24 +- Sources/Schema/XSDComplexContent.swift | 14 +- Sources/Schema/XSDContext.swift | 8 +- Sources/Schema/XSDDerivation.swift | 24 +- Sources/Schema/XSDDerivationTables.swift | 28 +- Sources/Schema/XSDFinding.swift | 15 + Sources/Schema/XSDFindingsCache.swift | 12 + Sources/Schema/XSDParserHelpers.swift | 52 ++- Sources/Schema/XSDParserParticles.swift | 4 +- Sources/Schema/XSDRuleToken.swift | 49 +++ Sources/Schema/XSDSimpleParser+Facets.swift | 20 +- Sources/Schema/XSDSimpleTypeOrder.swift | 8 +- Sources/Schema/XSDValidations.swift | 303 ++++++++++++++---- Sources/Schema/XSDWildcardParser.swift | 4 +- Sources/Validation/AttributeListParser.swift | 8 +- Sources/Validation/BuiltinValidation.swift | 84 ++++- .../BuiltinValidationXSDInstance.swift | 137 ++++++++ Sources/Validation/ContentModelParser.swift | 23 +- Sources/Validation/DTDDefaults.swift | 4 +- .../Validation/DTDStandaloneValidity.swift | 4 +- Sources/Validation/DTDValidationHelpers.swift | 4 +- Sources/Validation/Schematron.swift | 4 +- Sources/Validation/SchematronParser.swift | 8 +- .../Validation/StreamingDTDValidator.swift | 8 +- .../Validation/ValidationCombinators.swift | 22 +- Sources/Validation/Validator.swift | 4 +- Sources/XInclude/URIReference.swift | 24 +- Sources/XInclude/XIncludeProcessor.swift | 16 +- Sources/XPath/AxisNavigation.swift | 16 +- Sources/XPath/Compiler.swift | 12 +- Sources/XPath/CompilerPaths.swift | 12 +- Sources/XPath/CoreFunctions.swift | 8 +- Sources/XPath/DocumentOrder.swift | 12 +- Sources/XPath/EXSLTFunctions.swift | 4 +- Sources/XPath/Evaluator.swift | 8 +- Sources/XPath/EvaluatorOperators.swift | 8 +- Sources/XPath/EvaluatorSteps.swift | 24 +- Sources/XPath/NodeFunctions.swift | 4 +- Sources/XPath/StringFunctions.swift | 20 +- Sources/XPath/Value.swift | 16 +- Sources/XPointer/Pointer.swift | 24 +- Sources/XPointer/PointerRange.swift | 12 +- Sources/XSLT/XSLTAVT.swift | 7 +- Sources/XSLT/XSLTCollation.swift | 8 +- Sources/XSLT/XSLTDocumentCache.swift | 4 +- Sources/XSLT/XSLTElements.swift | 12 +- Sources/XSLT/XSLTFunctions.swift | 52 ++- Sources/XSLT/XSLTGlobals.swift | 8 +- Sources/XSLT/XSLTInstantiation.swift | 4 +- Sources/XSLT/XSLTInstantiationElements.swift | 12 +- Sources/XSLT/XSLTInstantiationSteps.swift | 16 +- Sources/XSLT/XSLTLookups.swift | 4 +- Sources/XSLT/XSLTMatchCache.swift | 4 +- Sources/XSLT/XSLTNumbering.swift | 40 ++- Sources/XSLT/XSLTParser.swift | 24 +- Sources/XSLT/XSLTParserDeclarations.swift | 16 +- Sources/XSLT/XSLTParserEntry.swift | 4 +- Sources/XSLT/XSLTParserStructural.swift | 4 +- Sources/XSLT/XSLTSorting.swift | 8 +- Sources/XSLT/XSLTTransform.swift | 16 +- Sources/XSLT/XSLTTransformer.swift | 28 +- Sources/XSLT/XSLTWhitespace.swift | 8 +- Tests/AttributeDocumentOrderTests.swift | 4 +- Tests/ByteFastPathTests.swift | 4 +- Tests/ContentModelOracle.swift | 16 +- Tests/ContentNFAStateBudgetTests.swift | 4 +- Tests/DTDSubsetStrictnessTests.swift | 6 +- Tests/DTDTests.swift | 16 +- Tests/DeterminismDifferentialTests.swift | 20 +- Tests/EncodingTests.swift | 4 +- Tests/EntityDecodeBulkTests.swift | 8 +- Tests/FacetValidationDifferentialTests.swift | 24 +- Tests/Fixtures/validation-field-registry.txt | 26 +- Tests/FollowingPrecedingAxisTests.swift | 4 +- Tests/HTMLForeignContentTests.swift | 4 +- Tests/HTMLTokenizerTests.swift | 8 +- .../InstanceValidationDifferentialTests.swift | 20 +- Tests/LineEndingTests.swift | 4 +- Tests/NamespaceTests.swift | 12 +- Tests/OutputEncodingTests.swift | 4 +- Tests/ParsingTests.swift | 4 +- Tests/PushParserTests.swift | 4 +- Tests/RelaxNGSpecSuiteTests.swift | 8 +- Tests/SAXTests.swift | 4 +- Tests/SchemaDifferentialHarness.swift | 24 +- Tests/SchemaXSTSDiagnosticTests.swift | 12 +- Tests/SchemaXSTSRegressionTests.swift | 16 +- Tests/SiblingAxisTests.swift | 4 +- Tests/ValidationConfigurationTests.swift | 36 ++- Tests/ValidationFieldCoverageTests.swift | 8 +- Tests/ValidationFrameworkTests.swift | 22 ++ Tests/ValidationRuleRegistryTests.swift | 37 ++- Tests/ValidationRuleTestSupport.swift | 41 +++ Tests/ValidationRuleXSDAttributeTests.swift | 205 ++++++++++++ Tests/ValidationRuleXSDContentTests.swift | 159 +++++++++ Tests/ValidationRuleXSDDerivationTests.swift | 121 +++++++ Tests/ValidationRuleXSDIdentityTests.swift | 260 +++++++++++++++ Tests/ValidationRuleXSDNilFixedTests.swift | 165 ++++++++++ Tests/ValidationRuleXSDStructureTests.swift | 133 ++++++++ Tests/ValidationRuleXSDTests.swift | 97 +----- Tests/XPathDescendantFusionTests.swift | 4 +- Tests/XPathFusedAttributeTests.swift | 4 +- Tests/XPathFusedDescendantTests.swift | 4 +- Tests/XPathSingleStepOrderTests.swift | 8 +- Tests/XSLTParityTests.swift | 4 +- Tests/XSLTParserTests.swift | 18 +- Tests/XSLTTests.swift | 4 +- Tests/XSTSDifferentialTests.swift | 4 +- Tests/XSTSSuiteTests.swift | 8 +- docs/validation-field-registry.txt | 26 +- 247 files changed, 4424 insertions(+), 1197 deletions(-) create mode 100644 Sources/Schema/ComplexValidatorNilFixed.swift create mode 100644 Sources/Schema/ComplexValidatorWildcards.swift create mode 100644 Sources/Schema/IdentityValidatorHelpers.swift create mode 100644 Sources/Schema/XSDFinding.swift create mode 100644 Sources/Schema/XSDFindingsCache.swift create mode 100644 Sources/Schema/XSDRuleToken.swift create mode 100644 Sources/Validation/BuiltinValidationXSDInstance.swift create mode 100644 Tests/ValidationRuleXSDAttributeTests.swift create mode 100644 Tests/ValidationRuleXSDContentTests.swift create mode 100644 Tests/ValidationRuleXSDDerivationTests.swift create mode 100644 Tests/ValidationRuleXSDIdentityTests.swift create mode 100644 Tests/ValidationRuleXSDNilFixedTests.swift create mode 100644 Tests/ValidationRuleXSDStructureTests.swift diff --git a/PublicAPITests/PublicSurfaceTests.swift b/PublicAPITests/PublicSurfaceTests.swift index 24e436d..c4cd9f0 100644 --- a/PublicAPITests/PublicSurfaceTests.swift +++ b/PublicAPITests/PublicSurfaceTests.swift @@ -39,7 +39,9 @@ struct PublicSurfaceTests { var chunks = ["", "hello", ""].makeIterator() var characters: [Character] = [] var reader = PureXML.Parsing.EventReader(pulling: { - if characters.isEmpty, let chunk = chunks.next() { characters = Array(chunk) } + if characters.isEmpty, let chunk = chunks.next() { + characters = Array(chunk) + } return characters.isEmpty ? nil : characters.removeFirst() }) var events = 0 diff --git a/Sources/Canonical/CanonicalNodeSubset.swift b/Sources/Canonical/CanonicalNodeSubset.swift index d9dd412..4697cca 100644 --- a/Sources/Canonical/CanonicalNodeSubset.swift +++ b/Sources/Canonical/CanonicalNodeSubset.swift @@ -18,7 +18,9 @@ extension PureXML.Canonical.Canonicalizer { let declared = Set(apex.attributes.map(\.name.description)) for (prefix, uri) in ancestorNamespaces { let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)" - if !declared.contains(name) { attributes.append(PureXML.Model.Attribute(name, uri)) } + if !declared.contains(name) { + attributes.append(PureXML.Model.Attribute(name, uri)) + } } } return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children) @@ -43,7 +45,9 @@ extension PureXML.Canonical.Canonicalizer { /// apex does not already set, nearest ancestor winning. static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] { let present = Set(apex.attributes.map(\.name.description)) - if mergeBase { return inherited11XMLAttributes(above: node, apex: apex, present: present) } + if mergeBase { + return inherited11XMLAttributes(above: node, apex: apex, present: present) + } // Canonical XML 1.0: the nearest of every in-scope xml:* attribute the // apex does not already set. var seen: Set = [] @@ -63,8 +67,12 @@ extension PureXML.Canonical.Canonicalizer { /// The prefix a namespace declaration binds (empty for the default /// namespace), or nil when the attribute is not a namespace declaration. static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? { - if name.prefix == nil, name.localName == "xmlns" { return "" } - if name.prefix == "xmlns" { return name.localName } + if name.prefix == nil, name.localName == "xmlns" { + return "" + } + if name.prefix == "xmlns" { + return name.localName + } return nil } } diff --git a/Sources/Canonical/CanonicalPrefixRewrite.swift b/Sources/Canonical/CanonicalPrefixRewrite.swift index be5e0d8..f255767 100644 --- a/Sources/Canonical/CanonicalPrefixRewrite.swift +++ b/Sources/Canonical/CanonicalPrefixRewrite.swift @@ -208,8 +208,12 @@ extension PureXML.Canonical { } private static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? { - if name.prefix == nil, name.localName == "xmlns" { return "" } - if name.prefix == "xmlns" { return name.localName } + if name.prefix == nil, name.localName == "xmlns" { + return "" + } + if name.prefix == "xmlns" { + return name.localName + } return nil } } diff --git a/Sources/Canonical/CanonicalURI.swift b/Sources/Canonical/CanonicalURI.swift index 1e3009d..398c797 100644 --- a/Sources/Canonical/CanonicalURI.swift +++ b/Sources/Canonical/CanonicalURI.swift @@ -5,7 +5,9 @@ extension PureXML.Canonical.Canonicalizer { static func inherited11XMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, present: Set) -> [PureXML.Model.Attribute] { var result: [PureXML.Model.Attribute] = [] for local in ["lang", "space"] where !present.contains("xml:\(local)") { - if let attribute = nearestXMLAttribute(local, above: node) { result.append(attribute) } + if let attribute = nearestXMLAttribute(local, above: node) { + result.append(attribute) + } } if let base = mergedBase(above: node, apex: apex) { result.append(PureXML.Model.Attribute("xml:base", base)) @@ -94,27 +96,35 @@ extension PureXML.Canonical.Canonicalizer { var input = Substring(path) var output = "" while !input.isEmpty { - if input.hasPrefix("../") { input = input.dropFirst(3) + if input.hasPrefix("../") { + input = input.dropFirst(3) continue } - if input.hasPrefix("./") { input = input.dropFirst(2) + if input.hasPrefix("./") { + input = input.dropFirst(2) continue } - if input.hasPrefix("/./") { input = "/" + input.dropFirst(3) + if input.hasPrefix("/./") { + input = "/" + input.dropFirst(3) continue } - if input == "/." { input = "/" + if input == "/." { + input = "/" continue } - if input.hasPrefix("/../") { input = "/" + input.dropFirst(4) + if input.hasPrefix("/../") { + input = "/" + input.dropFirst(4) removeLastSegment(&output) continue } - if input == "/.." { input = "/" + if input == "/.." { + input = "/" removeLastSegment(&output) continue } - if input == "." || input == ".." { break } + if input == "." || input == ".." { + break + } let start = input.hasPrefix("/") ? input.index(after: input.startIndex) : input.startIndex let next = input[start...].firstIndex(of: "/") ?? input.endIndex output += input[.." : "" } @@ -149,7 +153,9 @@ public extension PureXML.Canonical { let text = options.trimTextNodes ? value.trimmingXMLWhitespace() : value output += Self.escapeText(text) case let .comment(value): - if options.includeComments { output += "" } + if options.includeComments { + output += "" + } case let .processingInstruction(target, data): output += data.isEmpty ? "" : "" case .element, .document: @@ -198,7 +204,9 @@ public extension PureXML.Canonical { for prefix in utilized { let uri = inScope[prefix] ?? "" if prefix.isEmpty { - if uri != (rendered[""] ?? "") { result.append(("", uri)) } + if uri != (rendered[""] ?? "") { + result.append(("", uri)) + } } else if !uri.isEmpty, rendered[prefix] != uri { result.append((prefix, uri)) } @@ -215,15 +223,21 @@ public extension PureXML.Canonical { private static func attributeOrder(_ lhs: PureXML.Model.Attribute, _ rhs: PureXML.Model.Attribute) -> Bool { let leftURI = lhs.name.namespaceURI ?? "" let rightURI = rhs.name.namespaceURI ?? "" - if leftURI != rightURI { return leftURI < rightURI } + if leftURI != rightURI { + return leftURI < rightURI + } return lhs.name.localName < rhs.name.localName } private static func namespaceDeclarations(_ element: PureXML.Model.Element) -> [(String, String)] { element.attributes.compactMap { attribute in let name = attribute.name - if name.prefix == nil, name.localName == "xmlns" { return ("", attribute.value) } - if name.prefix == "xmlns" { return (name.localName, attribute.value) } + if name.prefix == nil, name.localName == "xmlns" { + return ("", attribute.value) + } + if name.prefix == "xmlns" { + return (name.localName, attribute.value) + } return nil } } @@ -327,7 +341,9 @@ public extension PureXML.Canonical.Canonicalizer { case .text, .cdata: output += Self.escapeText(options.trimTextNodes ? node.value.trimmingXMLWhitespace() : node.value) case .comment: - if options.includeComments { output += "" } + if options.includeComments { + output += "" + } case .processingInstruction: output += node.value.isEmpty ? "" : "" case .element, .document, .doctype, .entityReference, .namespace: diff --git a/Sources/Catalog/CatalogParser.swift b/Sources/Catalog/CatalogParser.swift index 1e53240..93347ca 100644 --- a/Sources/Catalog/CatalogParser.swift +++ b/Sources/Catalog/CatalogParser.swift @@ -27,7 +27,9 @@ private struct CatalogBuilder { insert(node, key: "systemId", base: base, into: &systemMap) case "public": insert(node, key: "publicId", base: base, into: &publicMap) - if let id = attribute(node, "publicId") { publicPrefer[id] = prefer } + if let id = attribute(node, "publicId") { + publicPrefer[id] = prefer + } case "uri": insert(node, key: "name", base: base, into: &uriMap) case "rewriteSystem": @@ -62,7 +64,9 @@ private struct CatalogBuilder { case "delegateURI": appendDelegate(node, start: "uriStartString", base: base, into: &delegateURI) case "nextCatalog": - if let catalog = attribute(node, "catalog") { nextCatalogs.append(resolve(catalog, base)) } + if let catalog = attribute(node, "catalog") { + nextCatalogs.append(resolve(catalog, base)) + } default: break } diff --git a/Sources/Catalog/Resolver.swift b/Sources/Catalog/Resolver.swift index 2f4c0a6..59beae4 100644 --- a/Sources/Catalog/Resolver.swift +++ b/Sources/Catalog/Resolver.swift @@ -163,16 +163,22 @@ public extension PureXML.Catalog { _ loadingCatalog: (String) -> String?, _ visited: inout Set, ) -> String? { - if let local = pick(self) { return local } + if let local = pick(self) { + return local + } let matching = delegates .filter { identifier.hasPrefix($0.startString) } .sorted { $0.startString.count > $1.startString.count } for rule in matching { - if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) { return resolved } + if let sub = load(rule.catalog, loadingCatalog, &visited), let resolved = pick(sub) { + return resolved + } } for next in nextCatalogs { guard let sub = load(next, loadingCatalog, &visited) else { continue } - if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) { return resolved } + if let resolved = sub.chained(identifier, delegates: [], pick: pick, loadingCatalog, &visited) { + return resolved + } } return nil } diff --git a/Sources/Catalog/SGMLCatalogParser.swift b/Sources/Catalog/SGMLCatalogParser.swift index 3078d2d..ad7b9fb 100644 --- a/Sources/Catalog/SGMLCatalogParser.swift +++ b/Sources/Catalog/SGMLCatalogParser.swift @@ -27,16 +27,22 @@ private struct SGMLBuilder { mutating func apply(_ keyword: String, _ scanner: inout SGMLScanner) { let upper = keyword.uppercased() - if applyPair(upper, &scanner) { return } + if applyPair(upper, &scanner) { + return + } applySingle(upper, &scanner) } private mutating func applyPair(_ keyword: String, _ scanner: inout SGMLScanner) -> Bool { switch keyword { case "PUBLIC": - if let id = scanner.next(), let uri = scanner.next() { publicMap[id] = resolve(uri) } + if let id = scanner.next(), let uri = scanner.next() { + publicMap[id] = resolve(uri) + } case "SYSTEM": - if let id = scanner.next(), let uri = scanner.next() { systemMap[id] = resolve(uri) } + if let id = scanner.next(), let uri = scanner.next() { + systemMap[id] = resolve(uri) + } case "DELEGATE": if let prefix = scanner.next(), let catalog = scanner.next() { delegatePublic.append(PureXML.Catalog.DelegateRule(startString: prefix, catalog: resolve(catalog))) @@ -50,11 +56,17 @@ private struct SGMLBuilder { private mutating func applySingle(_ keyword: String, _ scanner: inout SGMLScanner) { switch keyword { case "CATALOG": - if let catalog = scanner.next() { nextCatalogs.append(resolve(catalog)) } + if let catalog = scanner.next() { + nextCatalogs.append(resolve(catalog)) + } case "BASE": - if let declared = scanner.next() { base = resolve(declared) } + if let declared = scanner.next() { + base = resolve(declared) + } case "OVERRIDE": - if let value = scanner.next() { preferPublic = value.uppercased() == "YES" } + if let value = scanner.next() { + preferPublic = value.uppercased() == "YES" + } default: break } diff --git a/Sources/Emitting/Escaping.swift b/Sources/Emitting/Escaping.swift index 592d4a9..3be58ec 100644 --- a/Sources/Emitting/Escaping.swift +++ b/Sources/Emitting/Escaping.swift @@ -20,12 +20,16 @@ extension PureXML.Emitting { while let marker = value.utf8[runStart...].firstIndex(where: { textEscapableByte($0, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) }) { - if runStart < marker { result += value[runStart ..< marker] } + if runStart < marker { + result += value[runStart ..< marker] + } let character = value[marker] result += textEscape(character, asciiOnly: asciiOnly, escapeCarriageReturn: escapeCarriageReturn) ?? String(character) runStart = value.index(after: marker) } - if runStart < value.endIndex { result += value[runStart...] } + if runStart < value.endIndex { + result += value[runStart...] + } return result } @@ -98,12 +102,16 @@ extension PureXML.Emitting { while let marker = value.utf8[runStart...].firstIndex(where: { attributeEscapableByte($0, quoteByte: quoteByte, asciiOnly: asciiOnly) }) { - if runStart < marker { result += value[runStart ..< marker] } + if runStart < marker { + result += value[runStart ..< marker] + } let character = value[marker] result += attributeEscape(character, quote: quote, asciiOnly: asciiOnly) ?? String(character) runStart = value.index(after: marker) } - if runStart < value.endIndex { result += value[runStart...] } + if runStart < value.endIndex { + result += value[runStart...] + } return result } @@ -155,15 +163,21 @@ extension PureXML.Emitting { static func comment(_ value: String) -> String { // Fast path: a comment with no hyphen has no `--` and no trailing `-`, // so it is returned unchanged rather than rebuilt character by character. - if !value.utf8.contains(0x2D) { return value } + if !value.utf8.contains(0x2D) { + return value + } var result = "" var previousHyphen = false for character in value { - if character == "-", previousHyphen { result.append(" ") } + if character == "-", previousHyphen { + result.append(" ") + } result.append(character) previousHyphen = character == "-" } - if previousHyphen { result.append(" ") } + if previousHyphen { + result.append(" ") + } return result } @@ -173,11 +187,15 @@ extension PureXML.Emitting { static func processingInstruction(_ data: String) -> String { // Fast path: data with no '?' cannot contain `?>`, so it is returned // unchanged rather than rebuilt character by character. - if !data.utf8.contains(0x3F) { return data } + if !data.utf8.contains(0x3F) { + return data + } var result = "" var previousQuestion = false for character in data { - if character == ">", previousQuestion { result.append(" ") } + if character == ">", previousQuestion { + result.append(" ") + } result.append(character) previousQuestion = character == "?" } diff --git a/Sources/HTML/HTMLAdoptionAgency.swift b/Sources/HTML/HTMLAdoptionAgency.swift index 67612cb..e108861 100644 --- a/Sources/HTML/HTMLAdoptionAgency.swift +++ b/Sources/HTML/HTMLAdoptionAgency.swift @@ -37,8 +37,12 @@ extension HTMLDocument { /// scope boundary. private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool { for node in openBody.reversed() { - if node === target { return true } - if Self.scopeBoundaries.contains(tagName(node)) { return false } + if node === target { + return true + } + if Self.scopeBoundaries.contains(tagName(node)) { + return false + } } return false } @@ -48,7 +52,9 @@ extension HTMLDocument { /// re-wrapped (the `X` -> `X` behavior). func reconstructActiveFormatting() { guard var index = activeFormatting.indices.last else { return } - if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { return } + if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { + return + } while index > 0 { index -= 1 if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { @@ -91,7 +97,9 @@ extension HTMLDocument { activeFormatting.remove(at: formattingIndexInList) return } - if !hasInScope(formattingElement) { return } + if !hasInScope(formattingElement) { + return + } guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else { openBody.removeLast(openBody.count - formattingOpenIndex) activeFormatting.remove(at: formattingIndexInList) @@ -104,7 +112,9 @@ extension HTMLDocument { private func lastFormatting(tag: String) -> Int? { for index in activeFormatting.indices.reversed() { guard let entry = activeFormatting[index] else { return nil } // a marker bounds the search - if tagName(entry) == tag { return index } + if tagName(entry) == tag { + return index + } } return nil } @@ -124,7 +134,9 @@ extension HTMLDocument { inner += 1 nodeIndex -= 1 let node = openBody[nodeIndex] - if node === formattingElement { break } + if node === formattingElement { + break + } guard let activeIndex = formattingIndex(node) else { openBody.remove(at: nodeIndex) continue @@ -132,7 +144,9 @@ extension HTMLDocument { let clone = node.shallowCopy() activeFormatting[activeIndex] = clone openBody[nodeIndex] = clone - if lastNode === furthestBlock { bookmark = activeIndex + 1 } + if lastNode === furthestBlock { + bookmark = activeIndex + 1 + } clone.append(lastNode) lastNode = clone } @@ -144,13 +158,19 @@ extension HTMLDocument { furthestBlock.append(formattingClone) if let removeIndex = formattingIndex(formattingElement) { activeFormatting.remove(at: removeIndex) - if removeIndex < bookmark { bookmark -= 1 } + if removeIndex < bookmark { + bookmark -= 1 + } } activeFormatting.insert(formattingClone, at: min(bookmark, activeFormatting.count)) - if let removeOpen = openIndex(formattingElement) { openBody.remove(at: removeOpen) } + if let removeOpen = openIndex(formattingElement) { + openBody.remove(at: removeOpen) + } // The clone becomes a child of the furthest block, so it sits immediately // above it on the open-elements stack; inserting below would re-find the // same furthest block and loop. - if let blockIndex = openIndex(furthestBlock) { openBody.insert(formattingClone, at: blockIndex + 1) } + if let blockIndex = openIndex(furthestBlock) { + openBody.insert(formattingClone, at: blockIndex + 1) + } } } diff --git a/Sources/HTML/HTMLDocumentBuilder.swift b/Sources/HTML/HTMLDocumentBuilder.swift index ed63187..3cbba6c 100644 --- a/Sources/HTML/HTMLDocumentBuilder.swift +++ b/Sources/HTML/HTMLDocumentBuilder.swift @@ -84,8 +84,12 @@ final class HTMLDocument { } private func processInitial(_ token: Token) { - if case .doctype = token { return } - if isIgnorableWhitespace(token) { return } + if case .doctype = token { + return + } + if isIgnorableWhitespace(token) { + return + } mode = .beforeHtml process(token) } @@ -96,13 +100,17 @@ final class HTMLDocument { mode = .beforeHead return } - if isIgnorableWhitespace(token) { return } + if isIgnorableWhitespace(token) { + return + } mode = .beforeHead process(token) } private func processBeforeHead(_ token: Token) { - if isIgnorableWhitespace(token) { return } + if isIgnorableWhitespace(token) { + return + } if case let .startTag(name, attributes, _) = token, name == "head" { headAttributes = modelAttributes(attributes) mode = .inHead @@ -140,7 +148,9 @@ final class HTMLDocument { } let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes)) openHead.last?.append(element) - if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) { openHead.append(element) } + if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) { + openHead.append(element) + } } private func headClose(_ name: String) { @@ -168,7 +178,9 @@ final class HTMLDocument { process(token) return } - if isIgnorableWhitespace(token) { return } + if isIgnorableWhitespace(token) { + return + } mode = .inBody process(token) } @@ -210,7 +222,11 @@ final class HTMLDocument { case let .startTag(name, attributes, _) where name == "noframes": framesetStack.last?.append(PureXML.Model.TreeNode.element("noframes", attributes: modelAttributes(attributes))) case let .endTag(name) where name == "frameset": - if framesetStack.count > 1 { framesetStack.removeLast() } else { mode = .afterFrameset } + if framesetStack.count > 1 { + framesetStack.removeLast() + } else { + mode = .afterFrameset + } case let .comment(value): framesetStack.last?.append(.comment(value)) default: @@ -243,7 +259,9 @@ final class HTMLDocument { private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) { if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1 { openBody.removeLast(openBody.count - selectIndex) - if name == "select" { return } // a nested select just closes the open one + if name == "select" { + return + } // a nested select just closes the open one } if let selectIndex = selectInTableToClose(for: name) { openBody.removeLast(openBody.count - selectIndex) @@ -278,8 +296,12 @@ final class HTMLDocument { case "tr" where context == "table": bodyPushImplied("tbody") case "td", "th": - if context == "table" { bodyPushImplied("tbody") } - if context == "table" || ["tbody", "thead", "tfoot"].contains(context) { bodyPushImplied("tr") } + if context == "table" { + bodyPushImplied("tbody") + } + if context == "table" || ["tbody", "thead", "tfoot"].contains(context) { + bodyPushImplied("tr") + } default: break } @@ -303,11 +325,17 @@ final class HTMLDocument { /// The foreign-content namespace for a body element: SVG/MathML on entry, the /// nearest open foreign ancestor's, or HTML (nil) inside an integration point. private func bodyForeignNamespace(for name: String) -> String? { - if name == "svg" { return ForeignNamespace.svg } - if name == "math" { return ForeignNamespace.mathml } + if name == "svg" { + return ForeignNamespace.svg + } + if name == "math" { + return ForeignNamespace.mathml + } for node in openBody.reversed() { guard let namespace = node.name?.namespaceURI else { continue } - if Self.svgIntegrationPoints.contains(tagName(node)) { return nil } + if Self.svgIntegrationPoints.contains(tagName(node)) { + return nil + } return namespace } return nil @@ -330,7 +358,9 @@ extension HTMLDocument { } private func isIgnorableWhitespace(_ token: Token) -> Bool { - if case let .text(value) = token { return value.trimmingXMLWhitespace().isEmpty } + if case let .text(value) = token { + return value.trimmingXMLWhitespace().isEmpty + } return false } } diff --git a/Sources/HTML/HTMLEntities.swift b/Sources/HTML/HTMLEntities.swift index d726e4e..85f718d 100644 --- a/Sources/HTML/HTMLEntities.swift +++ b/Sources/HTML/HTMLEntities.swift @@ -59,13 +59,17 @@ extension PureXML.HTML.Tokenizer { private static func decodeNumeric(_ characters: [Character], at start: Int) -> (String, Int)? { var cursor = start + 2 // past "&#" let isHex = cursor < characters.count && (characters[cursor] == "x" || characters[cursor] == "X") - if isHex { cursor += 1 } + if isHex { + cursor += 1 + } let digitsStart = cursor while cursor < characters.count, isDigit(characters[cursor], hex: isHex) { cursor += 1 } guard cursor > digitsStart, let number = UInt32(String(characters[digitsStart ..< cursor]), radix: isHex ? 16 : 10) else { return nil } - if cursor < characters.count, characters[cursor] == ";" { cursor += 1 } + if cursor < characters.count, characters[cursor] == ";" { + cursor += 1 + } return (String(numericCharacter(number)), cursor - start) } diff --git a/Sources/HTML/HTMLSerializer.swift b/Sources/HTML/HTMLSerializer.swift index 3190392..83792fe 100644 --- a/Sources/HTML/HTMLSerializer.swift +++ b/Sources/HTML/HTMLSerializer.swift @@ -39,7 +39,9 @@ extension PureXML.HTML { output += ">" // A void element has no end tag and no content (the html model); // anything nested is dropped, as the recursive form did. - if Elements.void.contains(name.lowercased()) { return } + if Elements.void.contains(name.lowercased()) { + return + } stack.append(HTMLSerializeStep.close(name)) // A raw-text element (script, style) writes its own text children // verbatim; nested elements still serialize normally by their own @@ -57,7 +59,9 @@ extension PureXML.HTML { private static func attributeText(_ attribute: PureXML.Model.Attribute) -> String { let name = attribute.name.description - if attribute.value.isEmpty { return " \(name)" } + if attribute.value.isEmpty { + return " \(name)" + } // Boolean attributes minimize when the value repeats the name // (CHECKED="CHECKED" serializes as CHECKED), the html form. if Elements.booleanAttributes.contains(name.lowercased()), attribute.value.lowercased() == name.lowercased() { @@ -132,7 +136,9 @@ extension PureXML.HTML { default: result += latin1Entity(scalar) ?? String(Character(scalar)) } } - if previousWasAmpersand { result += "&" } + if previousWasAmpersand { + result += "&" + } return result } diff --git a/Sources/HTML/HTMLTokenizer.swift b/Sources/HTML/HTMLTokenizer.swift index d3da723..d690d78 100644 --- a/Sources/HTML/HTMLTokenizer.swift +++ b/Sources/HTML/HTMLTokenizer.swift @@ -27,10 +27,18 @@ extension PureXML.HTML { return rawText(until: raw) } guard peek() == "<" else { return text() } - if matches("") { .comment(String($0)) } } - if startsWith(buffer, "") { .cdata(String($0)) } } - if startsWith(buffer, "") { _ in .ignorable } } - if startsWith(buffer, "") { .comment(String($0)) } + } + if startsWith(buffer, "") { .cdata(String($0)) } + } + if startsWith(buffer, "") { _ in .ignorable } + } + if startsWith(buffer, "" { diff --git a/Sources/Parsing/Reader.swift b/Sources/Parsing/Reader.swift index f33ea3b..21d76fe 100644 --- a/Sources/Parsing/Reader.swift +++ b/Sources/Parsing/Reader.swift @@ -67,7 +67,9 @@ extension PureXML.Parsing { // Split multi-scalar graphemes so the buffer is scalar-level. var queued: [Character] = [] self.pull = { - if !queued.isEmpty { return queued.removeFirst() } + if !queued.isEmpty { + return queued.removeFirst() + } guard let next = pull() else { return nil } let scalars = next.unicodeScalars guard scalars.count > 1 else { return next } @@ -133,10 +135,14 @@ extension PureXML.Parsing { private mutating func normalizedPull() -> Character? { guard let character = rawNext() else { return nil } // Swift coalesces a CR+LF pair into one grapheme-cluster character. - if character == "\r\n" { return "\n" } + if character == "\r\n" { + return "\n" + } if character == "\r" { let next = rawNext() - if next != "\n", !(xml11 && next == "\u{85}") { pendingRaw = next } + if next != "\n", !(xml11 && next == "\u{85}") { + pendingRaw = next + } return "\n" } if xml11, character == "\u{85}" || character == "\u{2028}" { @@ -201,7 +207,9 @@ extension PureXML.Parsing { /// True if the upcoming characters equal `literal`, without consuming. mutating func matches(_ literal: String) -> Bool { if buffer.isEmpty, pendingRaw == nil, let storage { - if let byteResult = matchesBytes(literal, storage) { return byteResult } + if let byteResult = matchesBytes(literal, storage) { + return byteResult + } } // No per-call Array materialization: literals are short constants // and this runs on every scan decision (the hottest comparison in @@ -211,7 +219,9 @@ extension PureXML.Parsing { guard available >= count else { return false } var index = 0 for character in literal { - if buffer[head + index] != character { return false } + if buffer[head + index] != character { + return false + } index += 1 } return true @@ -254,10 +264,18 @@ extension PureXML.Parsing { let limit = byteIndex + maxLength while index < storage.count { let byte = pointer[index] - if byte >= 0x80 { return nil } - if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) { break } - if index >= limit { return nil } - if colon == nil, byte == 0x3A { colon = index - byteIndex } + if byte >= 0x80 { + return nil + } + if !PureXML.Parsing.XMLCharacter.isASCIINameChar(byte) { + break + } + if index >= limit { + return nil + } + if colon == nil, byte == 0x3A { + colon = index - byteIndex + } index += 1 } let length = index - byteIndex @@ -289,7 +307,9 @@ extension PureXML.Parsing { } mutating func skipSpace() { - if skipSpaceBytes() { return } + if skipSpaceBytes() { + return + } while let character = peek(), character.isXMLWhitespace { advance() } diff --git a/Sources/Parsing/ReaderByteRuns.swift b/Sources/Parsing/ReaderByteRuns.swift index 7075bd1..1e6f340 100644 --- a/Sources/Parsing/ReaderByteRuns.swift +++ b/Sources/Parsing/ReaderByteRuns.swift @@ -22,19 +22,25 @@ extension PureXML.Parsing.Reader { // the slow path consumed "]]" and the run would begin with '>': leave a // leading '>' to the slow path and its cdataCloseInContent check (W3C // ibm14n01). - if byteIndex < count, pointer[byteIndex] == 0x3E { return nil } + if byteIndex < count, pointer[byteIndex] == 0x3E { + return nil + } var index = byteIndex var newlines = 0 var lastLineStart = -1 while index < count { let byte = pointer[index] - if byte == 0x3C { break } // '<' + if byte == 0x3C { + break + } // '<' // Valid plain ASCII content only; CR, ']' and non-ASCII go to the // character path. guard byte < 0x80, byte != 0x0D, byte != 0x5D, byte >= 0x20 || byte == 0x09 || byte == 0x0A else { - if index == byteIndex { return nil } + if index == byteIndex { + return nil + } break } if byte == 0x0A { diff --git a/Sources/Parsing/ShiftJISDecoder.swift b/Sources/Parsing/ShiftJISDecoder.swift index d553067..fe3bec0 100644 --- a/Sources/Parsing/ShiftJISDecoder.swift +++ b/Sources/Parsing/ShiftJISDecoder.swift @@ -19,7 +19,9 @@ extension PureXML.Parsing.ByteDecoder { scalars.append(scalar) } else { scalars.append("\u{FFFD}") - if byte <= 0x7F { scalars.append(Unicode.Scalar(byte)) } // reprocess ASCII trail + if byte <= 0x7F { + scalars.append(Unicode.Scalar(byte)) + } // reprocess ASCII trail } } else if let single = singleByteScalar(byte) { scalars.append(single) @@ -29,15 +31,21 @@ extension PureXML.Parsing.ByteDecoder { scalars.append("\u{FFFD}") } } - if lead != 0 { scalars.append("\u{FFFD}") } + if lead != 0 { + scalars.append("\u{FFFD}") + } return String(scalars) } /// The scalar for a byte that is not part of a two-byte sequence: ASCII /// (`0x00`-`0x80`) or half-width katakana (`0xA1`-`0xDF`), else nil. static func singleByteScalar(_ byte: UInt8) -> Unicode.Scalar? { - if byte <= 0x80 { return Unicode.Scalar(byte) } - if (0xA1 ... 0xDF).contains(byte) { return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) } + if byte <= 0x80 { + return Unicode.Scalar(byte) + } + if (0xA1 ... 0xDF).contains(byte) { + return Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) + } return nil } @@ -53,7 +61,9 @@ extension PureXML.Parsing.ByteDecoder { let offset = lead < 0xA0 ? 0x81 : 0xC1 let trailOffset = trail < 0x7F ? 0x40 : 0x41 let pointer = (Int(lead) - offset) * 188 + Int(trail) - trailOffset - if (8836 ... 10715).contains(pointer) { return Unicode.Scalar(0xE000 + pointer - 8836) } // EUDC private use + if (8836 ... 10715).contains(pointer) { + return Unicode.Scalar(0xE000 + pointer - 8836) + } // EUDC private use guard pointer >= 0, pointer < jis0208.count, jis0208[pointer] != 0xFFFF else { return nil } return Unicode.Scalar(UInt32(jis0208[pointer])) } diff --git a/Sources/Parsing/StreamingDecoder.swift b/Sources/Parsing/StreamingDecoder.swift index 49b2f59..1d5f4eb 100644 --- a/Sources/Parsing/StreamingDecoder.swift +++ b/Sources/Parsing/StreamingDecoder.swift @@ -68,8 +68,12 @@ extension PureXML.Parsing { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } - if lead == 0x80 { return "\u{20AC}" } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } + if lead == 0x80 { + return "\u{20AC}" + } guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement } guard let trail = nextByte() else { finished = true return Self.replacement @@ -84,8 +88,12 @@ extension PureXML.Parsing { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } - if lead == 0x80 { return "\u{20AC}" } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } + if lead == 0x80 { + return "\u{20AC}" + } guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement } guard let second = nextByte() else { finished = true return Self.replacement @@ -108,7 +116,9 @@ extension PureXML.Parsing { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement } guard let trail = nextByte() else { finished = true return Self.replacement @@ -125,7 +135,9 @@ extension PureXML.Parsing { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement } guard let trail = nextByte() else { finished = true return Self.replacement @@ -140,7 +152,9 @@ extension PureXML.Parsing { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } if lead == 0x8E { guard let byte = nextByte() else { finished = true return Self.replacement @@ -167,7 +181,9 @@ extension PureXML.Parsing { finished = true return nil } - if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) { return Character(single) } + if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) { + return Character(single) + } guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement } guard let trail = nextByte() else { finished = true @@ -186,7 +202,9 @@ extension PureXML.Parsing { } ordered.append(UInt32(byte)) } - if !bigEndian { ordered.reverse() } + if !bigEndian { + ordered.reverse() + } let value = ordered.reduce(UInt32(0)) { ($0 << 8) | $1 } return Character(Unicode.Scalar(value) ?? "\u{FFFD}") } diff --git a/Sources/Parsing/StreamingDecoderEUCTW.swift b/Sources/Parsing/StreamingDecoderEUCTW.swift index 6c1336c..c8709cc 100644 --- a/Sources/Parsing/StreamingDecoderEUCTW.swift +++ b/Sources/Parsing/StreamingDecoderEUCTW.swift @@ -6,7 +6,9 @@ extension PureXML.Parsing.StreamingDecoder { finished = true return nil } - if lead <= 0x7F { return Character(Unicode.Scalar(lead)) } + if lead <= 0x7F { + return Character(Unicode.Scalar(lead)) + } if lead == 0x8E { guard let plane = nextByte(), let byte1 = nextByte(), let byte2 = nextByte() else { finished = true diff --git a/Sources/Parsing/TextReader.swift b/Sources/Parsing/TextReader.swift index 1ac1091..e775ba5 100644 --- a/Sources/Parsing/TextReader.swift +++ b/Sources/Parsing/TextReader.swift @@ -186,7 +186,9 @@ public extension PureXML.Parsing { applyStartElement(name: name, attributes: attributes) case let .endElement(name): currentDepth -= 1 - if !langStack.isEmpty { langStack.removeLast() } + if !langStack.isEmpty { + langStack.removeLast() + } currentName = name xmlLang = langStack.last ?? nil set(.endElement, name: name.description, depth: currentDepth) diff --git a/Sources/Parsing/XMLCharacter.swift b/Sources/Parsing/XMLCharacter.swift index f3ace93..83f2f51 100644 --- a/Sources/Parsing/XMLCharacter.swift +++ b/Sources/Parsing/XMLCharacter.swift @@ -67,7 +67,9 @@ public extension PureXML.Parsing { /// `NameChar`: a name character after the first. public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool { - if isNameStart(scalar) { return true } + if isNameStart(scalar) { + return true + } switch scalar.value { case 0x2D, // "-" 0x2E, // "." @@ -86,7 +88,9 @@ public extension PureXML.Parsing { var scalars = name.unicodeScalars.makeIterator() guard let first = scalars.next(), isNameStart(first) else { return false } while let scalar = scalars.next() { - if !isNameChar(scalar) { return false } + if !isNameChar(scalar) { + return false + } } return true } diff --git a/Sources/Pattern/Matcher.swift b/Sources/Pattern/Matcher.swift index 55283a3..275416b 100644 --- a/Sources/Pattern/Matcher.swift +++ b/Sources/Pattern/Matcher.swift @@ -22,7 +22,9 @@ extension PureXML.Pattern { case let .prefixWildcard(prefix): return name.prefix == prefix case let .name(text): - if text.contains(":") { return name.description == text } + if text.contains(":") { + return name.description == text + } return name.description == text || name.localName == text } } @@ -105,7 +107,9 @@ public extension PureXML.Pattern { private func ancestorMatches(_ stepIndex: Int, below pathIndex: Int, _ path: [PureXML.Model.QualifiedName]) -> Bool { var candidate = pathIndex - 1 while candidate >= 0 { - if matchUpTo(stepIndex, candidate, path) { return true } + if matchUpTo(stepIndex, candidate, path) { + return true + } candidate -= 1 } return false diff --git a/Sources/Pattern/PatternCompiler.swift b/Sources/Pattern/PatternCompiler.swift index 46ef437..19945af 100644 --- a/Sources/Pattern/PatternCompiler.swift +++ b/Sources/Pattern/PatternCompiler.swift @@ -42,8 +42,12 @@ extension PureXML.Pattern { } private mutating func leadingGap() -> Gap { - if consume("//") { return .descendant } - if consume("/") { return .root } + if consume("//") { + return .descendant + } + if consume("/") { + return .root + } return .descendant } @@ -60,11 +64,17 @@ extension PureXML.Pattern { private mutating func parseStep(gap: Gap) throws -> RawStep { guard let character = peek() else { throw PatternError.expectedStep } - if character == "[" { throw PatternError.unsupported("predicate") } - if character == "." { throw PatternError.unsupported(".") } + if character == "[" { + throw PatternError.unsupported("predicate") + } + if character == "." { + throw PatternError.unsupported(".") + } let isAttribute = consume("@") let test = try parseTest() - if peek() == "[" { throw PatternError.unsupported("predicate") } + if peek() == "[" { + throw PatternError.unsupported("predicate") + } return RawStep(gap: gap, isAttribute: isAttribute, test: test) } @@ -116,7 +126,9 @@ extension PureXML.Pattern { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } } } diff --git a/Sources/Pattern/PatternStream.swift b/Sources/Pattern/PatternStream.swift index 9d0a1e1..2b721d3 100644 --- a/Sources/Pattern/PatternStream.swift +++ b/Sources/Pattern/PatternStream.swift @@ -29,7 +29,9 @@ public extension PureXML.Pattern.Matcher { stack.append(name) collect(stack: stack, attributes: attributes, into: &results) case .endElement: - if !stack.isEmpty { stack.removeLast() } + if !stack.isEmpty { + stack.removeLast() + } default: break } diff --git a/Sources/Regex/RegexAutomaton.swift b/Sources/Regex/RegexAutomaton.swift index feae68e..f7b34b2 100644 --- a/Sources/Regex/RegexAutomaton.swift +++ b/Sources/Regex/RegexAutomaton.swift @@ -30,7 +30,9 @@ extension PureXML.Regex { next.insert(transition.target) } } - if next.isEmpty { return false } + if next.isEmpty { + return false + } current = closure(next) } return current.contains(accept) @@ -104,7 +106,11 @@ extension PureXML.Regex { var last: Int? for node in nodes { let part = fragment(for: node) - if let previous = last { states[previous].epsilon.append(part.start) } else { first = part.start } + if let previous = last { + states[previous].epsilon.append(part.start) + } else { + first = part.start + } last = part.accept } return (first ?? addState(), last ?? addState()) @@ -127,7 +133,9 @@ extension PureXML.Regex { let boundedMin = Swift.min(minimum, Self.repeatUnrollCap) var unrolled = 0 for _ in 0 ..< boundedMin { - if stateBudgetExhausted { break } + if stateBudgetExhausted { + break + } let part = fragment(for: node) states[current].epsilon.append(part.start) current = part.accept diff --git a/Sources/Regex/RegexCategory.swift b/Sources/Regex/RegexCategory.swift index 1700f5c..c0e405e 100644 --- a/Sources/Regex/RegexCategory.swift +++ b/Sources/Regex/RegexCategory.swift @@ -23,7 +23,9 @@ extension PureXML.Regex { /// a two-letter category (`Lu`, `Nd`, ...), or an `Is` block name. enum CategoryMatcher { static func isKnown(_ name: String) -> Bool { - if name.hasPrefix("Is") { return blocks[String(name.dropFirst(2))] != nil } + if name.hasPrefix("Is") { + return blocks[String(name.dropFirst(2))] != nil + } return groups.contains(name) || codeValues.contains(name) } diff --git a/Sources/Regex/RegexParser.swift b/Sources/Regex/RegexParser.swift index 2f68f89..6b74c69 100644 --- a/Sources/Regex/RegexParser.swift +++ b/Sources/Regex/RegexParser.swift @@ -20,7 +20,9 @@ extension PureXML.Regex { } static func parse(_ pattern: String) throws -> Node { - if pattern.isEmpty { return .empty } + if pattern.isEmpty { + return .empty + } var parser = RegexParser(pattern) let node = try parser.parseAlternation() guard parser.isAtEnd else { throw RegexError.unbalanced } @@ -41,7 +43,9 @@ extension PureXML.Regex { while let character = peek(), character != "|", character != ")" { try pieces.append(parsePiece()) } - if pieces.isEmpty { return .empty } + if pieces.isEmpty { + return .empty + } return pieces.count == 1 ? pieces[0] : .concat(pieces) } @@ -75,7 +79,9 @@ extension PureXML.Regex { maximum = peek() == "}" ? nil : parseInteger() } guard peek() == "}", let minimum else { throw RegexError.badQuantifier } - if let maximum, minimum > maximum { throw RegexError.reversedQuantifier } + if let maximum, minimum > maximum { + throw RegexError.reversedQuantifier + } advance() return .repeated(atom, min: minimum, max: maximum) } @@ -186,7 +192,9 @@ extension PureXML.Regex { cls.negated = true } // A class must have at least one member; `[]` (or `[^]`) is invalid. - if peek() == "]" { throw RegexError.emptyClass } + if peek() == "]" { + throw RegexError.emptyClass + } while let character = peek(), character != "]" { if character == "-", peek(1) == "[" { advance() @@ -236,13 +244,19 @@ extension PureXML.Regex { // A nested `[` is legal only as a `-[...]` subtraction, routed by // parseClass/parseClassMember before reaching here; any other `[` // is an unescaped class bracket, invalid per Appendix F. - if character == "[" { throw RegexError.unescapedClassBracket } + if character == "[" { + throw RegexError.unescapedClassBracket + } return .scalar(scalar(of: character)) } guard let escaped = peek() else { throw RegexError.incompleteEscape } advance() - if let single = Self.singleEscape(escaped) { return .scalar(single) } - if let member = Self.classEscape(escaped) { return member } + if let single = Self.singleEscape(escaped) { + return .scalar(single) + } + if let member = Self.classEscape(escaped) { + return member + } if escaped == "p" || escaped == "P" { return try .category(parseCategory(negated: escaped == "P")) } @@ -274,7 +288,9 @@ extension PureXML.Regex { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } } } diff --git a/Sources/Regex/RegexSyntax.swift b/Sources/Regex/RegexSyntax.swift index 7d22896..81f3def 100644 --- a/Sources/Regex/RegexSyntax.swift +++ b/Sources/Regex/RegexSyntax.swift @@ -121,7 +121,9 @@ extension PureXML.Regex { // removes from that result: `[^cde-[ag]]` is (not c/d/e) minus a/g, // i.e. excludes {a,c,d,e,g}, not just {c,d,e} (XSTS RegexTest_430). var hit = negated ? !inBase : inBase - if hit, subtraction.contains(where: { $0.matches(scalar) }) { hit = false } + if hit, subtraction.contains(where: { $0.matches(scalar) }) { + hit = false + } return hit } diff --git a/Sources/Regex/XSDCategory.swift b/Sources/Regex/XSDCategory.swift index 1c53fe9..5830091 100644 --- a/Sources/Regex/XSDCategory.swift +++ b/Sources/Regex/XSDCategory.swift @@ -47,7 +47,9 @@ extension PureXML.Regex { ] static func generalCategoryCode(_ scalar: Unicode.Scalar) -> String { - if let override = categoryOverrides[scalar.value] { return override } + if let override = categoryOverrides[scalar.value] { + return override + } guard assignedInUnicode32(scalar.value) else { return "Cn" } return categoryCodes[scalar.properties.generalCategory] ?? "Cn" } diff --git a/Sources/Schema/BuiltinType.swift b/Sources/Schema/BuiltinType.swift index 36adc27..c2ba507 100644 --- a/Sources/Schema/BuiltinType.swift +++ b/Sources/Schema/BuiltinType.swift @@ -92,7 +92,9 @@ public extension PureXML.Schema { func derives(from other: BuiltinType) -> Bool { var current: BuiltinType? = self while let type = current { - if type == other { return true } + if type == other { + return true + } current = type.derivationBase } return false @@ -108,8 +110,12 @@ public extension PureXML.Schema { } var primitive: Primitive { - if let primitive = Self.stringLike[self] { return primitive } - if Self.integerLike.contains(self) { return .integer } + if let primitive = Self.stringLike[self] { + return primitive + } + if Self.integerLike.contains(self) { + return .integer + } return computedPrimitive } diff --git a/Sources/Schema/ComplexType.swift b/Sources/Schema/ComplexType.swift index 6028e3f..4e81f9c 100644 --- a/Sources/Schema/ComplexType.swift +++ b/Sources/Schema/ComplexType.swift @@ -121,9 +121,15 @@ public extension PureXML.Schema { return .notAbsent case let (.notNamespace(name), .enumerated(set)), let (.enumerated(set), .notNamespace(name)): let hasName = set.contains(name), hasAbsent = set.contains("") - if hasName, hasAbsent { return .any } // E1-10 5.1.1 - if hasName { return .notAbsent } // E1-10 5.1.2 - if hasAbsent { return nil } // NOT EXPRESSIBLE, E1-10 5.1.3 + if hasName, hasAbsent { + return .any + } // E1-10 5.1.1 + if hasName { + return .notAbsent + } // E1-10 5.1.2 + if hasAbsent { + return nil + } // NOT EXPRESSIBLE, E1-10 5.1.3 return .notNamespace(name) // E1-10 5.1.4 case let (.notAbsent, .enumerated(set)), let (.enumerated(set), .notAbsent): return set.contains("") ? .any : .notAbsent @@ -131,8 +137,12 @@ public extension PureXML.Schema { } private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents { - if left == .strict || right == .strict { return .strict } - if left == .lax || right == .lax { return .lax } + if left == .strict || right == .strict { + return .strict + } + if left == .lax || right == .lax { + return .lax + } return .skip } @@ -191,8 +201,12 @@ public extension PureXML.Schema { // When the two differ the intersection is strictly not expressible; keep // the less strict so a matched attribute is never validated more harshly // than either source asked for (under-validation, not over-rejection). - if left == right { return left } - if left == .skip || right == .skip { return .skip } + if left == right { + return left + } + if left == .skip || right == .skip { + return .skip + } return .lax } } @@ -292,7 +306,9 @@ public extension PureXML.Schema { /// The fixed value, when this is a `fixed` constraint. public var fixedValue: String? { - if case let .fixed(value) = self { return value } + if case let .fixed(value) = self { + return value + } return nil } diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift index acece36..c4d6e1c 100644 --- a/Sources/Schema/ComplexValidator.swift +++ b/Sources/Schema/ComplexValidator.swift @@ -23,9 +23,9 @@ public extension PureXML.Schema { /// The named type table that `ElementType.typeReference` resolves against. let types: [String: ElementType] /// Local names of elements declared `nillable="true"`. - private let nillableElements: Set + let nillableElements: Set /// The `default`/`fixed` value constraint declared on each element name. - private let elementConstraints: [String: ValueConstraint] + let elementConstraints: [String: ValueConstraint] /// Complex types declared `abstract="true"`, keyed by namespaced identity /// (`{ns}local`) so same-local-name types in different namespaces stay distinct. let abstractTypes: Set @@ -79,8 +79,21 @@ public extension PureXML.Schema { at path: [PureXML.Validation.PathKey] = [], namespaceBindings inheritedBindings: [String: String] = [:], ) -> [PureXML.Validation.ValidationError] { + validateTagged(element, against: type, at: path, namespaceBindings: inheritedBindings).map(\.error) + } + + /// The tagged form of ``validate(_:against:at:namespaceBindings:)``: the same + /// computation, with each finding labeled by the decomposed rule it belongs + /// to. The public, untagged form is a thin `.map(\.error)` over this so + /// behavior and output stay identical for every existing caller. + func validateTagged( + _ element: PureXML.Model.Element, + against type: ComplexType, + at path: [PureXML.Validation.PathKey] = [], + namespaceBindings inheritedBindings: [String: String] = [:], + ) -> [XSDFinding] { let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings) - var errors: [XSDFailure] = [] + var errors: [XSDFinding] = [] validateAttributes(element, type, at: path, into: &errors) // An xsi:nil element keeps its attribute obligations but must have no // content; its content model is not otherwise checked. @@ -90,68 +103,15 @@ public extension PureXML.Schema { validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors) // A simpleContent fixed value is compared in its simple type's value space // (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5". - let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { simple } else { nil } + let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { + simple + } else { + nil + } errors += elementFixedErrors(element, valueType: simpleContentType, at: path) return errors } - /// The errors from an `xsi:nil="true"` element: rejecting it when the - /// element is not nillable, or when it carries content. Returns nil when - /// the element is not nilled. - func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFailure]? { - let name = element.name.localName - // cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at - // all, whatever its value (so `xsi:nil="false"` is equally forbidden, not - // only `xsi:nil="true"`). - if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) { - return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)] - } - guard Self.isNil(element) else { return nil } - if Self.hasContent(element) { - return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)] - } - // cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}. - // The matched particle's constraint is authoritative; the by-local-name map - // is a fallback for paths without a particle (e.g. the resolved-type entry). - if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil { - return [XSDFailure(reason: "nil element '\(name)' must not have a fixed value constraint", at: path)] - } - return [] - } - - /// The text an element's simple content is validated against: its character - /// content, or, when the element is empty, its `default`/`fixed` value (an - /// empty element takes that value, and the value, not the empty string, must - /// satisfy the type). The matched particle's constraint takes precedence over - /// the by-local-name `elementConstraints` map, exactly as the tree path's - /// `validateChild` resolves it. - func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: ValueConstraint? = nil) -> String { - let text = Self.rawTextContent(element) - let constraint = particleConstraint ?? elementConstraints[element.name.localName] - return text.isEmpty ? (constraint?.value ?? text) : text - } - - /// The error from a `fixed` element value constraint: the element's text - /// must equal the fixed value in the element type's value space when known. - func elementFixedErrors( - _ element: PureXML.Model.Element, - valueType: SimpleType? = nil, - particleFixed: String? = nil, - at path: XSDPath, - ) -> [XSDFailure] { - guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] } - // A fixed element's content is its fixed character value: it may not carry - // element children, even under a mixed content type (cvc-elt.5.2.2.2.1). - if !element.children.compactMap(\.element).isEmpty { - return [XSDFailure(reason: "fixed element '\(element.name.localName)' must not have element children", at: path)] - } - let text = Self.rawTextContent(element) - // An empty element takes the fixed value; only present content must equal it. - if text.isEmpty { return [] } - if let valueType, valueType.valueMatches(text, literal: fixed) { return [] } - return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)] - } - /// Validates `element` against any element type at `path`, resolving a /// `typeReference` through the type table. public func validate( @@ -159,7 +119,16 @@ public extension PureXML.Schema { as type: ElementType, at path: [PureXML.Validation.PathKey] = [], ) -> [PureXML.Validation.ValidationError] { - var errors: [XSDFailure] = [] + validateTagged(element, as: type, at: path).map(\.error) + } + + /// The tagged form of ``validate(_:as:at:)``. See ``validateTagged(_:against:at:namespaceBindings:)``. + func validateTagged( + _ element: PureXML.Model.Element, + as type: ElementType, + at path: [PureXML.Validation.PathKey] = [], + ) -> [XSDFinding] { + var errors: [XSDFinding] = [] let namespaceBindings = Self.namespaceBindings(for: element) validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors) return errors @@ -172,17 +141,21 @@ public extension PureXML.Schema { _ content: ContentType, at path: XSDPath, namespaceBindings: [String: String], - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { let children = element.children.compactMap(\.element) switch content { case .empty: - if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) } + if !children.isEmpty { + errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path)) + } rejectText(element, at: path, into: &errors) case let .simpleContent(type): - if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) } + if !children.isEmpty { + errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path)) + } if let error = type.validate(Self.rawTextContent(element)) { - errors.append(XSDFailure(reason: "content: \(error)", at: path)) + errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path)) } case let .elementOnly(particle): rejectText(element, at: path, into: &errors) @@ -192,9 +165,9 @@ public extension PureXML.Schema { } } - func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) { + func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFinding]) { guard !Self.textContent(element).isEmpty else { return } - errors.append(XSDFailure(reason: "element must not contain text", at: path)) + errors.append(XSDFinding(.textForbidden, reason: "element must not contain text", at: path)) } // MARK: Particles @@ -204,7 +177,7 @@ public extension PureXML.Schema { children: [PureXML.Model.Element], at path: XSDPath, namespaceBindings: [String: String], - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { // Locate each content-model violation at the offending child (or the // missing one), with a recovery hint, rather than one opaque failure, @@ -232,7 +205,7 @@ public extension PureXML.Schema { _ children: [PureXML.Model.Element], matched: [PureXML.Schema.MatchedParticle?], frame: XSDChildValidationFrame, - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { let steps = Self.childSteps(children) for (index, child) in children.enumerated() { @@ -254,60 +227,13 @@ public extension PureXML.Schema { } } - /// Validates a wildcard-matched child by its `processContents`: skip does - /// nothing, lax validates against a global declaration when one exists, and - /// strict requires one. - private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? { - // A global element declaration is always in the schema's target - // namespace, so an instance element matches one only by full qualified - // name. The namespaced key already covers a no-namespace global (its key - // is `element:{}local`); matching an unqualified instance against a - // namespaced global by bare local name would conflate namespaces, e.g. - // accepting unqualified text where a namespaced `xs:short` is declared. - types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] - } - - private func validateWildcardChild( - _ child: PureXML.Model.Element, - processContents: ProcessContents, - at path: XSDPath, - namespaceBindings: [String: String], - into errors: inout [XSDFailure], - ) { - switch processContents { - case .skip: - return - case .lax: - if let declaration = globalElementDeclaration(for: child.name) { - validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors) - } - case .strict: - if let declaration = globalElementDeclaration(for: child.name) { - validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors) - } else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) { - validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors) - } else { - errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path)) - } - } - } - - /// The type named by a wildcard-matched element's `xsi:type`, when present. - private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? { - guard Self.xsiTypeAttributeValue(child) != nil else { return nil } - guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings), - let type = Self.resolveNamedType(reference, in: types) - else { return nil } - return type - } - - private func validateChild( + func validateChild( _ child: PureXML.Model.Element, against declared: ElementType, at path: XSDPath, namespaceBindings: [String: String], particleConstraint: ValueConstraint? = nil, - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { // An instance `xsi:type` overrides the declared type, provided the named // type exists in the schema. Resolve to the type itself rather than a @@ -315,7 +241,7 @@ public extension PureXML.Schema { // the declared type can forbid the substitution; an abstract declared // type requires one. if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { - errors.append(xsiError) + errors.append(XSDFinding(.xsiTypeSubstitutionValid, xsiError)) return } guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return } @@ -328,26 +254,30 @@ public extension PureXML.Schema { // violation. Checked before the nil short-circuit, since an // xsi:nil element is equally bound by it. for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) { - errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path)) + errors.append(XSDFinding( + .simpleTypeDisallowedAttribute, + reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", + at: path, + )) } if let nilErrors = nilErrors(child, fixedValue: particleConstraint?.fixedValue, at: path) { errors += nilErrors return } if !child.children.compactMap(\.element).isEmpty { - errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path)) + errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(child.name.localName)' must not have children", at: path)) } // An empty element takes its `default`/`fixed` value; that value // (not the empty string) is what must be valid against the type, // including any xsi:type override already resolved into `simple`. let text = Self.rawTextContent(child) if let error = simple.validate(effectiveSimpleText(for: child, particleConstraint: particleConstraint)) { - errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path)) + errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(child.name.localName)': \(error)", at: path)) } recordIDs(simple, value: text, at: path) errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path) case let .complex(complex): - errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings)) + errors.append(contentsOf: validateTagged(child, against: complex, at: path, namespaceBindings: namespaceBindings)) case .typeReference: validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors) } @@ -360,13 +290,13 @@ public extension PureXML.Schema { _ child: PureXML.Model.Element, at path: XSDPath, namespaceBindings: [String: String], - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { switch resolveReference(declared) { case let .unknown(name): - errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path)) + errors.append(XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path)) case let .circular(name): - errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path)) + errors.append(XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path)) case let .resolved(resolved): validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors) } diff --git a/Sources/Schema/ComplexValidatorAttributes.swift b/Sources/Schema/ComplexValidatorAttributes.swift index 04f2240..c470fa6 100644 --- a/Sources/Schema/ComplexValidatorAttributes.swift +++ b/Sources/Schema/ComplexValidatorAttributes.swift @@ -3,21 +3,21 @@ extension PureXML.Schema.ComplexValidator { _ element: PureXML.Model.Element, _ type: PureXML.Schema.ComplexType, at path: [PureXML.Validation.PathKey], - into errors: inout [PureXML.Validation.ValidationError], + into errors: inout [XSDFinding], ) { let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) } for use in type.attributes where !use.prohibited { let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) } if let match { if let error = use.type.validate(match.value) { - errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)': \(error)", at: path)) + errors.append(XSDFinding(.attributeTypeValid, reason: "attribute '\(use.name.localName)': \(error)", at: path)) } if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) { - errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path)) + errors.append(XSDFinding(.attributeFixedMatches, reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path)) } recordIDs(use.type, value: match.value, at: path) } else if use.required { - errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path)) + errors.append(XSDFinding(.attributeRequiredPresent, reason: "missing required attribute '\(use.name.localName)'", at: path)) } else if let constraint = use.valueConstraint { // An absent attribute takes its `default`/`fixed` value, so a // defaulted or fixed IDREF/IDREFS must still resolve to a matching @@ -28,7 +28,8 @@ 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( + errors.append(XSDFinding( + .attributeSingleID, reason: "an element may carry at most one attribute of type ID", at: path, )) @@ -45,7 +46,9 @@ extension PureXML.Schema.ComplexValidator { on element: PureXML.Model.Element, ) -> Int { present.reduce(into: 0) { count, attribute in - if isIDTyped(attribute, type, on: element) { count += 1 } + if isIDTyped(attribute, type, on: element) { + count += 1 + } } } @@ -73,7 +76,9 @@ extension PureXML.Schema.ComplexValidator { use: PureXML.Schema.AttributeUse, on elementName: PureXML.Model.QualifiedName, ) -> Bool { - if sameName(instance, declared) { return true } + if sameName(instance, declared) { + return true + } guard use.chameleonUnprefixed, instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true, let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty @@ -86,7 +91,7 @@ extension PureXML.Schema.ComplexValidator { element: PureXML.Model.Element, type: PureXML.Schema.ComplexType, at path: [PureXML.Validation.PathKey], - into errors: inout [PureXML.Validation.ValidationError], + into errors: inout [XSDFinding], ) { // A `use="prohibited"` use is excluded from the effective {attribute uses}, // so a prohibited attribute is treated as undeclared and left to the @@ -96,7 +101,7 @@ extension PureXML.Schema.ComplexValidator { let wildcard = type.attributeWildcard for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) { guard let wildcard, wildcard.admits(attribute.name) else { - errors.append(PureXML.Validation.ValidationError(reason: "undeclared attribute '\(attribute.name.localName)'", at: path)) + errors.append(XSDFinding(.attributeDeclaredOrPermitted, reason: "undeclared attribute '\(attribute.name.localName)'", at: path)) continue } switch wildcard.processContents { @@ -108,7 +113,8 @@ extension PureXML.Schema.ComplexValidator { } case .strict: guard let use = globalAttributeUse(for: attribute.name) else { - errors.append(PureXML.Validation.ValidationError( + errors.append(XSDFinding( + .wildcardAttributeUndeclared, reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'", at: path, )) @@ -127,13 +133,13 @@ extension PureXML.Schema.ComplexValidator { _ attribute: PureXML.Model.Attribute, use: PureXML.Schema.AttributeUse, at path: [PureXML.Validation.PathKey], - into errors: inout [PureXML.Validation.ValidationError], + into errors: inout [XSDFinding], ) { if let error = use.type.validate(attribute.value) { - errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)': \(error)", at: path)) + errors.append(XSDFinding(.wildcardAttributeTypeValid, reason: "attribute '\(attribute.name.localName)': \(error)", at: path)) } if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) { - errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path)) + errors.append(XSDFinding(.wildcardAttributeFixedMatches, reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path)) } recordIDs(use.type, value: attribute.value, at: path) } diff --git a/Sources/Schema/ComplexValidatorContentModel.swift b/Sources/Schema/ComplexValidatorContentModel.swift index 9ef6cbf..dced8ef 100644 --- a/Sources/Schema/ComplexValidatorContentModel.swift +++ b/Sources/Schema/ComplexValidatorContentModel.swift @@ -4,7 +4,7 @@ extension PureXML.Schema.ComplexValidator { /// Walks the children through the content automaton, flagging the first child /// the follow-set rejects, or the missing content when the sequence ends early. - func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) { + func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) { let matcher = PureXML.Schema.ContentRTNMatcher(PureXML.Schema.ContentRTNBuilder.build(particle)) let steps = Self.childSteps(children) // Advance one active state-set across the children rather than re-walking @@ -14,19 +14,23 @@ extension PureXML.Schema.ComplexValidator { for (index, child) in children.enumerated() { guard let next = matcher.step(current, over: child.name) else { let allowed = matcher.admissible(from: current) - errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]])) + errors.append(XSDFinding( + .contentModelStructure, + reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", + at: path + [steps[index]], + )) return } current = next } if !matcher.isComplete(current) { - errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path)) + errors.append(XSDFinding(.contentModelStructure, reason: "content is incomplete\(Self.expectation(matcher.admissible(from: current)))", at: path)) } } /// Locates `all`-group violations: each child that is not an in-bounds member, /// recovering past it, then each required member that never appeared. - func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFailure]) { + func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], groupOptional: Bool = false, at path: XSDPath, into errors: inout [XSDFinding]) { var counts = [Int](repeating: 0, count: group.particles.count) let steps = Self.childSteps(children) for (index, child) in children.enumerated() { @@ -35,7 +39,7 @@ extension PureXML.Schema.ComplexValidator { let room = member.maxOccurs.map { counts[slot] < $0 } ?? true return room && Self.memberMatches(member.term, child.name) }) else { - errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]])) + errors.append(XSDFinding(.contentModelStructure, reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]])) continue } counts[position] += 1 @@ -44,10 +48,12 @@ extension PureXML.Schema.ComplexValidator { // absent (zero occurrences), so its required members are not expected; but // once any child appears the group is present once and every member's own // `minOccurs` applies. A required group (`minOccurs="1"`) always enforces. - if groupOptional, children.isEmpty { return } + if groupOptional, children.isEmpty { + return + } for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs { if case let .element(name, _, _, _, _, _) = member.term { - errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path)) + errors.append(XSDFinding(.contentModelStructure, reason: "element '\(name.localName)' is required but missing", at: path)) } } } @@ -92,7 +98,9 @@ extension PureXML.Schema.ComplexValidator { /// A "; expected a, b" hint naming the elements the automaton accepts next. static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String { let names = labels.compactMap { label -> String? in - if case let .name(qualified) = label { return "<\(qualified.localName)>" } + if case let .name(qualified) = label { + return "<\(qualified.localName)>" + } return nil } let unique = Set(names).sorted() diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift index 475eac1..f4b2962 100644 --- a/Sources/Schema/ComplexValidatorDerivation.swift +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -14,12 +14,24 @@ extension PureXML.Schema.ComplexValidator { at path: [PureXML.Validation.PathKey], namespaceBindings: [String: String], ) -> PureXML.Validation.ValidationError? { - if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return blocked } - if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return notDerived } - if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return anyTypeSub } - if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { return listUnion } - if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { return abstractType } - if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) } + if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { + return blocked + } + if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { + return notDerived + } + if let anyTypeSub = anyTypeForAnySimpleTypeError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { + return anyTypeSub + } + if let listUnion = listUnionSubstitutionError(declared: declared, child: child, at: path, namespaceBindings: namespaceBindings) { + return listUnion + } + if let abstractType = abstractXsiTypeError(child: child, at: path, namespaceBindings: namespaceBindings) { + return abstractType + } + if case let .typeReference(name) = declared { + return abstractTypeError(named: name, child: child, at: path) + } return nil } @@ -99,7 +111,9 @@ extension PureXML.Schema.ComplexValidator { case .simple, .complex: declared case let .typeReference(reference): resolvedDeclaredType(reference) } - if case let .simple(type)? = resolved { return type.isAnySimpleType } + if case let .simple(type)? = resolved { + return type.isAnySimpleType + } return false } @@ -157,7 +171,9 @@ extension PureXML.Schema.ComplexValidator { /// Whether an element type resolves to a complex type. static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool { - if case .complex = type { return true } + if case .complex = type { + return true + } return false } @@ -237,13 +253,17 @@ extension PureXML.Schema.ComplexValidator { for member in members { // The substitute IS a member: derived with no method of its own, so // this path is unblocked and the substitution is permitted outright. - if member == substitute { return false } + if member == substitute { + return false + } guard let toMember = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: member, typeDerivation) else { continue } anyConfirmedPath = true // An unblocked path to any member makes the whole substitution valid. - if toMember.isDisjoint(with: blocked) { return false } + if toMember.isDisjoint(with: blocked) { + return false + } } // Every confirmed member path is blocked (and at least one exists): bar it. return anyConfirmedPath @@ -325,8 +345,12 @@ extension PureXML.Schema.ComplexValidator { /// namespaced backbone and the check stays silent rather than risk a false /// positive. static func derivationKey(fromReference reference: String) -> String { - if reference.hasPrefix("type:") { return String(reference.dropFirst("type:".count)) } - if reference.hasPrefix("{") || reference.contains(":") { return reference } + if reference.hasPrefix("type:") { + return String(reference.dropFirst("type:".count)) + } + if reference.hasPrefix("{") || reference.contains(":") { + return reference + } return key(PureXML.Model.QualifiedName(localName: reference, namespaceURI: nil)) } } diff --git a/Sources/Schema/ComplexValidatorHelpers.swift b/Sources/Schema/ComplexValidatorHelpers.swift index 46d36cd..6cafb25 100644 --- a/Sources/Schema/ComplexValidatorHelpers.swift +++ b/Sources/Schema/ComplexValidatorHelpers.swift @@ -22,7 +22,9 @@ extension PureXML.Schema.ComplexValidator { return wildcard.processContents case let .group(group): for member in group.particles { - if let found = wildcard(in: member.term) { return found } + if let found = wildcard(in: member.term) { + return found + } } return nil case .element: @@ -39,7 +41,9 @@ extension PureXML.Schema.ComplexValidator { fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) { switch term { case let .element(name, type, _, _, _, _): - if let type { result[key(name)] = type } + if let type { + result[key(name)] = type + } case let .group(group): for member in group.particles { collectTypes(member.term, into: &result) @@ -62,7 +66,9 @@ extension PureXML.Schema.ComplexValidator { fileprivate static func collectValueConstraints(_ term: XSDTerm, into result: inout [String: PureXML.Schema.ValueConstraint]) { switch term { case let .element(name, _, _, valueConstraint, _, _): - if let valueConstraint { result[key(name)] = valueConstraint } + if let valueConstraint { + result[key(name)] = valueConstraint + } case let .group(group): for member in group.particles { collectValueConstraints(member.term, into: &result) @@ -200,7 +206,13 @@ extension PureXML.Schema.ComplexValidator { /// Whether the element has any child element or non-whitespace text. static func hasContent(_ element: PureXML.Model.Element) -> Bool { - !textContent(element).isEmpty || element.children.contains { if case .element = $0 { true } else { false } } + !textContent(element).isEmpty || element.children.contains { + if case .element = $0 { + true + } else { + false + } + } } } @@ -236,7 +248,9 @@ extension PureXML.Schema.ComplexValidator { /// Resolves a named type reference, including built-in datatypes and ur-types. static func resolveNamedType(_ name: String, in types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? { - if let next = types[name] { return next } + if let next = types[name] { + return next + } if name.hasPrefix("type:"), let (uri, local) = parseQualifiedTypeKey(name) { if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) { return builtin diff --git a/Sources/Schema/ComplexValidatorNilFixed.swift b/Sources/Schema/ComplexValidatorNilFixed.swift new file mode 100644 index 0000000..fd59771 --- /dev/null +++ b/Sources/Schema/ComplexValidatorNilFixed.swift @@ -0,0 +1,62 @@ +extension PureXML.Schema.ComplexValidator { + /// The errors from an `xsi:nil="true"` element: rejecting it when the + /// element is not nillable, or when it carries content. Returns nil when + /// the element is not nilled. + func nilErrors(_ element: PureXML.Model.Element, fixedValue: String? = nil, at path: XSDPath) -> [XSDFinding]? { + let name = element.name.localName + // cvc-elt.3.1: a non-nillable element must carry no `xsi:nil` attribute at + // all, whatever its value (so `xsi:nil="false"` is equally forbidden, not + // only `xsi:nil="true"`). + if Self.nilAttributeValue(element) != nil, !nillableElements.contains(name) { + return [XSDFinding(.nilAttributeRequiresNillable, reason: "element '\(name)' is not nillable but has xsi:nil", at: path)] + } + guard Self.isNil(element) else { return nil } + if Self.hasContent(element) { + return [XSDFinding(.nilElementMustBeEmpty, reason: "nil element '\(name)' must be empty", at: path)] + } + // cvc-elt.3.2.2: a nilled element may not have a fixed {value constraint}. + // The matched particle's constraint is authoritative; the by-local-name map + // is a fallback for paths without a particle (e.g. the resolved-type entry). + if (fixedValue ?? elementConstraints[name]?.fixedValue) != nil { + return [XSDFinding(.nilElementNoFixedConstraint, reason: "nil element '\(name)' must not have a fixed value constraint", at: path)] + } + return [] + } + + /// The text an element's simple content is validated against: its character + /// content, or, when the element is empty, its `default`/`fixed` value (an + /// empty element takes that value, and the value, not the empty string, must + /// satisfy the type). The matched particle's constraint takes precedence over + /// the by-local-name `elementConstraints` map, exactly as the tree path's + /// `validateChild` resolves it. + func effectiveSimpleText(for element: PureXML.Model.Element, particleConstraint: PureXML.Schema.ValueConstraint? = nil) -> String { + let text = Self.rawTextContent(element) + let constraint = particleConstraint ?? elementConstraints[element.name.localName] + return text.isEmpty ? (constraint?.value ?? text) : text + } + + /// The error from a `fixed` element value constraint: the element's text + /// must equal the fixed value in the element type's value space when known. + func elementFixedErrors( + _ element: PureXML.Model.Element, + valueType: PureXML.Schema.SimpleType? = nil, + particleFixed: String? = nil, + at path: XSDPath, + ) -> [XSDFinding] { + guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] } + // A fixed element's content is its fixed character value: it may not carry + // element children, even under a mixed content type (cvc-elt.5.2.2.2.1). + if !element.children.compactMap(\.element).isEmpty { + return [XSDFinding(.fixedElementNoChildren, reason: "fixed element '\(element.name.localName)' must not have element children", at: path)] + } + let text = Self.rawTextContent(element) + // An empty element takes the fixed value; only present content must equal it. + if text.isEmpty { + return [] + } + if let valueType, valueType.valueMatches(text, literal: fixed) { + return [] + } + return text == fixed ? [] : [XSDFinding(.fixedElementValueMatches, reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)] + } +} diff --git a/Sources/Schema/ComplexValidatorStreaming.swift b/Sources/Schema/ComplexValidatorStreaming.swift index 12efdc6..12176fa 100644 --- a/Sources/Schema/ComplexValidatorStreaming.swift +++ b/Sources/Schema/ComplexValidatorStreaming.swift @@ -17,6 +17,8 @@ public extension PureXML.Schema { } } +extension PureXML.Schema.ComplexValidator: PureXML.Validation.Validatable {} + public extension PureXML.Schema.ComplexValidator { // MARK: Streaming support @@ -36,6 +38,19 @@ public extension PureXML.Schema.ComplexValidator { } } + /// The streaming path's document-scoped ID/IDREF check as a composable + /// ``PureXML/Validation`` value, reusing ``idErrors()`` (the same accumulator + /// the tree path's `idIdrefConsistent` rule reads) rather than the streaming + /// driver calling it imperatively outside the framework. The validator is its + /// own subject: by the time this runs (after the whole document has streamed + /// through ``shallowValidity``), its `idTracker` already holds every recorded + /// ID/IDREF. + static var idConsistency: PureXML.Validation.Validation { + .init(description: "Every streamed xs:ID is unique and every streamed xs:IDREF resolves to one") { context in + context.subject.idErrors() + } + } + /// The matched particle's `default`/`fixed` value constraint for a child of /// `parent`'s content model, keyed by namespaced identity (so same-local-name /// elements in different types do not collide as in the flat `elementConstraints`). @@ -109,7 +124,9 @@ public extension PureXML.Schema.ComplexValidator { guard case let .complex(complex) = parent else { return nil } switch complex.content { case let .elementOnly(particle), let .mixed(particle): - if Self.elementTypes(in: particle.term)[Self.key(child)] != nil { return nil } + if Self.elementTypes(in: particle.term)[Self.key(child)] != nil { + return nil + } guard Self.wildcardMatch(for: child, in: particle.term) == .strict, types[PureXML.Schema.XSDParser.elementDeclarationKey(child)] == nil else { return nil } @@ -131,7 +148,9 @@ public extension PureXML.Schema.ComplexValidator { return nil case let .group(group): for member in group.particles { - if let match = wildcardMatch(for: name, in: member.term) { return match } + if let match = wildcardMatch(for: name, in: member.term) { + return match + } } return nil } @@ -160,28 +179,45 @@ public extension PureXML.Schema.ComplexValidator { valueConstraint: PureXML.Schema.ValueConstraint? = nil, at path: [PureXML.Validation.PathKey] = [], ) -> [PureXML.Validation.ValidationError] { - var errors: [XSDFailure] = [] + validateShallowTagged(element, as: type, valueConstraint: valueConstraint, at: path).map(\.error) + } + + /// The tagged form of ``validateShallow(_:as:valueConstraint:at:)``. See + /// ``PureXML/Schema/ComplexValidator/validateTagged(_:against:at:namespaceBindings:)``. + private func validateShallowTagged( + _ element: PureXML.Model.Element, + as type: PureXML.Schema.ElementType, + valueConstraint: PureXML.Schema.ValueConstraint? = nil, + at path: [PureXML.Validation.PathKey] = [], + ) -> [XSDFinding] { + var errors: [XSDFinding] = [] switch type { case let .simple(simple): validateSimpleElement(element, simple, valueConstraint: valueConstraint, at: path, into: &errors) case let .complex(complex): validateAttributes(element, complex, at: path, into: &errors) - if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors } + if let nilErrors = nilErrors(element, at: path) { + return errors + nilErrors + } shallowContent(element, complex.content, at: path, into: &errors) // A simpleContent fixed value is compared in its simple type's value space // (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:). - let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { simple } else { nil } + let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { + simple + } else { + nil + } errors += elementFixedErrors(element, valueType: simpleContentType, at: path) case .typeReference: // The shared resolver also guards a circular chain, which would // otherwise recurse here without terminating. switch resolveReference(type) { case let .unknown(name): - return [XSDFailure(reason: "unknown type '\(name)'", at: path)] + return [XSDFinding(.typeReferenceResolves, reason: "unknown type '\(name)'", at: path)] case let .circular(name): - return [XSDFailure(reason: "circular type reference '\(name)'", at: path)] + return [XSDFinding(.typeReferenceResolves, reason: "circular type reference '\(name)'", at: path)] case let .resolved(resolved): - return validateShallow(element, as: resolved, valueConstraint: valueConstraint, at: path) + return validateShallowTagged(element, as: resolved, valueConstraint: valueConstraint, at: path) } } return errors @@ -192,21 +228,21 @@ public extension PureXML.Schema.ComplexValidator { _ simple: PureXML.Schema.SimpleType, valueConstraint: PureXML.Schema.ValueConstraint? = nil, at path: XSDPath, - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) { if let nilErrors = nilErrors(element, fixedValue: valueConstraint?.fixedValue, at: path) { errors += nilErrors return } if !element.children.compactMap(\.element).isEmpty { - errors.append(XSDFailure(reason: "'\(element.name.localName)' must not have children", at: path)) + errors.append(XSDFinding(.simpleTypeForbidsChildren, reason: "'\(element.name.localName)' must not have children", at: path)) } // An empty element takes its `default`/`fixed` value; that value (not the // empty string) is what must be valid against the type, matching the tree // path's validateChild. let text = Self.rawTextContent(element) if let error = simple.validate(effectiveSimpleText(for: element, particleConstraint: valueConstraint)) { - errors.append(XSDFailure(reason: "'\(element.name.localName)': \(error)", at: path)) + errors.append(XSDFinding(.simpleTypeValueValid, reason: "'\(element.name.localName)': \(error)", at: path)) } recordIDs(simple, value: text, at: path) // Compare a present fixed value in the type's value space (e.g. "05" == "5" @@ -215,17 +251,21 @@ public extension PureXML.Schema.ComplexValidator { errors += elementFixedErrors(element, valueType: simple, particleFixed: valueConstraint?.fixedValue, at: path) } - private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFailure]) { + private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.ContentType, at path: XSDPath, into errors: inout [XSDFinding]) { let children = element.children.compactMap(\.element) switch content { case .empty: - if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) } + if !children.isEmpty { + errors.append(XSDFinding(.emptyContentForbidsChildren, reason: "element must be empty", at: path)) + } rejectText(element, at: path, into: &errors) case let .simpleContent(type): - if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) } + if !children.isEmpty { + errors.append(XSDFinding(.simpleContentForbidsChildren, reason: "element must not have children", at: path)) + } let text = Self.rawTextContent(element) if let error = type.validate(text) { - errors.append(XSDFailure(reason: "content: \(error)", at: path)) + errors.append(XSDFinding(.simpleContentTypeValid, reason: "content: \(error)", at: path)) } recordIDs(type, value: text, at: path) case let .elementOnly(particle): @@ -236,7 +276,7 @@ public extension PureXML.Schema.ComplexValidator { } } - private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) { + private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFinding]) { if case let .group(group) = particle.term, group.compositor == .all { allStructureErrors(group, children: children, groupOptional: particle.occurrenceRange.minimum.isZero, at: path, into: &errors) } else { diff --git a/Sources/Schema/ComplexValidatorWildcards.swift b/Sources/Schema/ComplexValidatorWildcards.swift new file mode 100644 index 0000000..3bdd453 --- /dev/null +++ b/Sources/Schema/ComplexValidatorWildcards.swift @@ -0,0 +1,48 @@ +extension PureXML.Schema.ComplexValidator { + /// A global element declaration is always in the schema's target + /// namespace, so an instance element matches one only by full qualified + /// name. The namespaced key already covers a no-namespace global (its key + /// is `element:{}local`); matching an unqualified instance against a + /// namespaced global by bare local name would conflate namespaces, e.g. + /// accepting unqualified text where a namespaced `xs:short` is declared. + func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? { + types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] + } + + /// Validates a wildcard-matched child by its `processContents`: skip does + /// nothing, lax validates against a global declaration when one exists, and + /// strict requires one. + func validateWildcardChild( + _ child: PureXML.Model.Element, + processContents: PureXML.Schema.ProcessContents, + at path: XSDPath, + namespaceBindings: [String: String], + into errors: inout [XSDFinding], + ) { + switch processContents { + case .skip: + return + case .lax: + if let declaration = globalElementDeclaration(for: child.name) { + validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors) + } + case .strict: + if let declaration = globalElementDeclaration(for: child.name) { + validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors) + } else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) { + validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors) + } else { + errors.append(XSDFinding(.wildcardElementUndeclared, reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path)) + } + } + } + + /// The type named by a wildcard-matched element's `xsi:type`, when present. + func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> PureXML.Schema.ElementType? { + guard Self.xsiTypeAttributeValue(child) != nil else { return nil } + guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings), + let type = Self.resolveNamedType(reference, in: types) + else { return nil } + return type + } +} diff --git a/Sources/Schema/ComplexValidatorXSIType.swift b/Sources/Schema/ComplexValidatorXSIType.swift index 2534b51..4fb8c87 100644 --- a/Sources/Schema/ComplexValidatorXSIType.swift +++ b/Sources/Schema/ComplexValidatorXSIType.swift @@ -7,20 +7,26 @@ extension PureXML.Schema.ComplexValidator { for child: PureXML.Model.Element, namespaceBindings: [String: String] = [:], at path: XSDPath, - into errors: inout [XSDFailure], + into errors: inout [XSDFinding], ) -> PureXML.Schema.ElementType? { guard Self.xsiTypeAttributeValue(child) != nil else { return declared } let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings) ?? Self.xsiTypeName(child) ?? "" - if let resolved = Self.resolveNamedType(reference, in: types) { return resolved } + if let resolved = Self.resolveNamedType(reference, in: types) { + return resolved + } let overriding = Self.xsiTypeName(child) ?? reference if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) { if xsiBuiltinSubstitutes(builtin, for: declared) { return .simple(PureXML.Schema.SimpleType(base: builtin)) } - errors.append(XSDFailure(reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'", at: path)) + errors.append(XSDFinding( + .xsiTypeOverrideResolves, + reason: "xsi:type '\(overriding)' is not validly derived from the declared type of '\(child.name.localName)'", + at: path, + )) return nil } - errors.append(XSDFailure(reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path)) + errors.append(XSDFinding(.xsiTypeOverrideResolves, reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path)) return nil } @@ -32,7 +38,9 @@ extension PureXML.Schema.ComplexValidator { private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool { switch declared { case let .typeReference(name): - if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") { return true } + if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") { + return true + } switch resolveReference(declared) { case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved) case .unknown, .circular: return false diff --git a/Sources/Schema/ContentMatcher.swift b/Sources/Schema/ContentMatcher.swift index 6f27327..9dbbabd 100644 --- a/Sources/Schema/ContentMatcher.swift +++ b/Sources/Schema/ContentMatcher.swift @@ -140,7 +140,9 @@ extension PureXML.Schema { func admissible(from current: Set) -> [TermLabel] { var labels: [TermLabel] = [] for configuration in current where states[configuration.state].target != nil { - if let label = states[configuration.state].label { labels.append(label) } + if let label = states[configuration.state].label { + labels.append(label) + } } return labels } @@ -203,7 +205,9 @@ extension PureXML.Schema { next.insert(ContentConfiguration(state: target, counters: configuration.counters)) } } - if next.isEmpty { return nil } + if next.isEmpty { + return nil + } return closure(next, caps: counterCaps(for: inputLength)) } diff --git a/Sources/Schema/ContentModelDeterminism.swift b/Sources/Schema/ContentModelDeterminism.swift index 7d19681..9b693e5 100644 --- a/Sources/Schema/ContentModelDeterminism.swift +++ b/Sources/Schema/ContentModelDeterminism.swift @@ -63,8 +63,12 @@ public extension PureXML.Schema { /// foreign annotation content. private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } - if local == "complexType" { visit(node) } + if local == "appinfo" || local == "documentation" { + return + } + if local == "complexType" { + visit(node) + } for child in PureXML.Schema.XSDNode.elementChildren(node) { forEachComplexType(child, visit) } diff --git a/Sources/Schema/ContentModelDeterminismCompositional.swift b/Sources/Schema/ContentModelDeterminismCompositional.swift index 0ab17b7..a3c1cea 100644 --- a/Sources/Schema/ContentModelDeterminismCompositional.swift +++ b/Sources/Schema/ContentModelDeterminismCompositional.swift @@ -98,7 +98,9 @@ private struct CompWalk { var identityConflict: String? mutating func emit(_ set: [CompItem]) { - if set.count > 1 { decisionSets.append(set) } + if set.count > 1 { + decisionSets.append(set) + } } /// Dedupe a decision set (first set or follow set) by particle identity. A repeated @@ -156,13 +158,17 @@ private struct CompWalk { } private func labelName(_ label: PureXML.Schema.TermLabel) -> String { - if case let .name(qname) = label { return qname.localName } + if case let .name(qname) = label { + return qname.localName + } return "any" } mutating func summarize(_ node: PureXML.Model.TreeNode, visiting: Set) -> CompSummary { let (minimum, maximum) = PureXML.Schema.XSDNode.occurrence(node) - if let maximum, maximum <= 0 { return CompSummary(nullable: true, first: [], lastFollow: []) } + if let maximum, maximum <= 0 { + return CompSummary(nullable: true, first: [], lastFollow: []) + } var body = bodySummary(node, visiting: visiting) // A repeat edge folds the body's first into every end-item's follow. Kept in a // separate method, not inline: `summarize` recurses on nesting depth, so any @@ -227,7 +233,9 @@ private struct CompWalk { var suffix: [CompItem] = [] for next in parts[(index + 1)...] { suffix.append(contentsOf: next.first) - if !next.nullable { break } + if !next.nullable { + break + } } suffixFirst[index] = suffix } @@ -239,7 +247,9 @@ private struct CompWalk { var prefixFirsts: [[CompItem]] = [] for part in parts { prefixFirsts.append(part.first) - if !part.nullable { break } + if !part.nullable { + break + } } // Each trailing end-item carries its internal follow PLUS the suffix that follows // it inside the sequence. Otherwise an end-item that is also followed internally @@ -253,7 +263,9 @@ private struct CompWalk { for entry in parts[index].lastFollow { trailing.append((entry.item, dedupOnly([entry.follow, suffixFirst[index]]))) } - if !parts[index].nullable { break } + if !parts[index].nullable { + break + } } return CompSummary(nullable: parts.allSatisfy(\.nullable), first: mergeList(prefixFirsts), lastFollow: mergeLastFollow(trailing)) } @@ -293,8 +305,12 @@ private struct CompWalk { private mutating func group(_ node: PureXML.Model.TreeNode, visiting: Set) -> CompSummary { guard let ref = PureXML.Schema.XSDNode.attribute(node, "ref") else { return CompSummary(nullable: true, first: [], lastFollow: []) } let name = PureXML.Schema.XSDNode.stripPrefix(ref) - if visiting.contains(name) { return redefineSelfReference(name, visiting: visiting) } - if let cached = groupCache[name] { return cached } + if visiting.contains(name) { + return redefineSelfReference(name, visiting: visiting) + } + if let cached = groupCache[name] { + return cached + } guard let definition = context.groups[name], let model = modelChild(definition) else { return CompSummary(nullable: true, first: [], lastFollow: []) } // Summarize the group once: its internal decision sets are context-independent diff --git a/Sources/Schema/ContentModelDeterminismConflict.swift b/Sources/Schema/ContentModelDeterminismConflict.swift index 737ef10..3a2f33c 100644 --- a/Sources/Schema/ContentModelDeterminismConflict.swift +++ b/Sources/Schema/ContentModelDeterminismConflict.swift @@ -11,8 +11,12 @@ enum CompConflict { for item in set { switch item.label { case let .name(qname): - if let owner = nameOwner[qname], owner != item.particle { return qname.localName } - if nameOwner[qname] == nil { nameOwner[qname] = item.particle } + if let owner = nameOwner[qname], owner != item.particle { + return qname.localName + } + if nameOwner[qname] == nil { + nameOwner[qname] = item.particle + } case let .wildcard(card): wildcards.append((card, item.particle)) } @@ -20,7 +24,9 @@ enum CompConflict { for (index, wildcard) in wildcards.enumerated() { // A wildcard conflicts with any name it admits, from a distinct particle, // and with any other wildcard it intersects, from a distinct particle. - if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) { return "any" } + if nameOwner.contains(where: { wildcard.card.admits($0.key) && $0.value != wildcard.particle }) { + return "any" + } for other in wildcards[(index + 1)...] where other.particle != wildcard.particle && wildcardsOverlap(wildcard.card, other.card) { return "any" } diff --git a/Sources/Schema/ContentModelEffectiveModel.swift b/Sources/Schema/ContentModelEffectiveModel.swift index a8f1ce4..35d3349 100644 --- a/Sources/Schema/ContentModelEffectiveModel.swift +++ b/Sources/Schema/ContentModelEffectiveModel.swift @@ -3,10 +3,14 @@ extension PureXML.Schema.ContentModelDeterminism { /// complex type's content model, reached through `complexContent` and its /// `restriction`/`extension`, or nil for simple or empty content. static func contentModelNode(_ complexType: XSDTree) -> XSDTree? { - if let direct = modelGroupChild(complexType) { return direct } + if let direct = modelGroupChild(complexType) { + return direct + } guard let complexContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent") else { return nil } for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) { - if let model = modelGroupChild(derivation) { return model } + if let model = modelGroupChild(derivation) { + return model + } } return nil } diff --git a/Sources/Schema/ContentModelGroupDependency.swift b/Sources/Schema/ContentModelGroupDependency.swift index 28f8f24..428e288 100644 --- a/Sources/Schema/ContentModelGroupDependency.swift +++ b/Sources/Schema/ContentModelGroupDependency.swift @@ -28,7 +28,9 @@ enum GroupDependencyOrder { order.append(name) continue } - if visited.contains(name) { continue } + if visited.contains(name) { + continue + } visited.insert(name) onPath.insert(name) work.append((name, true)) diff --git a/Sources/Schema/ContentNFABuilder.swift b/Sources/Schema/ContentNFABuilder.swift index b355201..f37ab3e 100644 --- a/Sources/Schema/ContentNFABuilder.swift +++ b/Sources/Schema/ContentNFABuilder.swift @@ -108,7 +108,11 @@ extension PureXML.Schema { var nullable = true for member in particles { let part = particle(member) - if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start } + if let previous = last { + addEpsilon(from: previous, to: part.start) + } else { + first = part.start + } last = part.accept fragmentCounters.append(contentsOf: part.counters) nullable = nullable && part.nullable diff --git a/Sources/Schema/ContentRTN.swift b/Sources/Schema/ContentRTN.swift index 2f0eb81..dc2d8b8 100644 --- a/Sources/Schema/ContentRTN.swift +++ b/Sources/Schema/ContentRTN.swift @@ -139,7 +139,9 @@ extension PureXML.Schema { /// The shared fragment for a group definition, compiled once and memoized. /// Its accept is marked `isReturn`. private mutating func groupFragment(_ group: PureXML.Schema.Group, id: Int) -> ContentNFAFragment { - if let cached = fragments[id] { return cached } + if let cached = fragments[id] { + return cached + } // `definitionID` groups are acyclic: XSD forbids a group referencing itself // except through an element, and the parser's expansion breaks every cycle at // an element boundary, so a fragment is never referenced during its own @@ -176,7 +178,11 @@ extension PureXML.Schema { var nullable = true for member in particles { let part = particle(member) - if let previous = last { addEpsilon(from: previous, to: part.start) } else { first = part.start } + if let previous = last { + addEpsilon(from: previous, to: part.start) + } else { + first = part.start + } last = part.accept fragmentCounters.append(contentsOf: part.counters) nullable = nullable && part.nullable diff --git a/Sources/Schema/ContentRTNMatcher.swift b/Sources/Schema/ContentRTNMatcher.swift index 923ea47..9fbcf44 100644 --- a/Sources/Schema/ContentRTNMatcher.swift +++ b/Sources/Schema/ContentRTNMatcher.swift @@ -109,7 +109,9 @@ extension PureXML.Schema { next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss)) } } - if next.isEmpty { return nil } + if next.isEmpty { + return nil + } position += 1 return closure(next) } @@ -121,7 +123,9 @@ extension PureXML.Schema { func admissible(from current: Set) -> [TermLabel] { var labels: [TermLabel] = [] for configuration in current where rtn.states[configuration.state].target != nil { - if let label = rtn.states[configuration.state].label { labels.append(label) } + if let label = rtn.states[configuration.state].label { + labels.append(label) + } } return labels } @@ -154,8 +158,12 @@ extension PureXML.Schema { let state = configuration.state guard let label = rtn.states[state].label, let target = rtn.states[state].target, label.matches(name) else { continue } next.insert(ContentRTNConfiguration(state: target, counters: configuration.counters, gss: configuration.gss)) - if anyState.map({ state < $0 }) ?? true { anyState = state } - if case .name = label, namedState.map({ state < $0 }) ?? true { namedState = state } + if anyState.map({ state < $0 }) ?? true { + anyState = state + } + if case .name = label, namedState.map({ state < $0 }) ?? true { + namedState = state + } } guard let chosenState = namedState ?? anyState, !next.isEmpty else { result.append(contentsOf: Array(repeating: nil, count: names.count - result.count)) @@ -190,7 +198,9 @@ extension PureXML.Schema { while true { if !pending.isEmpty { let configuration = pending.removeLast() - if seen.insert(configuration).inserted { work.append(configuration) } + if seen.insert(configuration).inserted { + work.append(configuration) + } continue } guard let configuration = work.popLast() else { break } @@ -203,7 +213,9 @@ extension PureXML.Schema { for call in state.calls { let node = internNode(returnState: call.returnTo, savedCounters: configuration.counters, parent: configuration.gss) let nextConfiguration = ContentRTNConfiguration(state: call.calleeStart, counters: configuration.counters, gss: node) - if seen.insert(nextConfiguration).inserted { work.append(nextConfiguration) } + if seen.insert(nextConfiguration).inserted { + work.append(nextConfiguration) + } drainPending(into: &seen, work: &work) } if state.isReturn, configuration.gss != 0 { @@ -218,7 +230,9 @@ extension PureXML.Schema { private func drainPending(into seen: inout Set, work: inout [ContentRTNConfiguration]) { while let configuration = pending.popLast() { - if seen.insert(configuration).inserted { work.append(configuration) } + if seen.insert(configuration).inserted { + work.append(configuration) + } } } diff --git a/Sources/Schema/DateTimeParser.swift b/Sources/Schema/DateTimeParser.swift index 3f415a2..5e2fed2 100644 --- a/Sources/Schema/DateTimeParser.swift +++ b/Sources/Schema/DateTimeParser.swift @@ -95,7 +95,9 @@ extension PureXML.Schema { guard let hour = readFixed(2), consume(":"), let minute = readFixed(2), consume(":"), let second = readFixed(2) else { return false } guard (0 ... 24).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return false } - if hour == 24, minute != 0 || second != 0 { return false } + if hour == 24, minute != 0 || second != 0 { + return false + } value.hour = hour value.minute = minute value.second = second @@ -107,12 +109,15 @@ extension PureXML.Schema { } private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool { - if consume("Z") { value.offsetMinutes = 0 + if consume("Z") { + value.offsetMinutes = 0 return true } guard peek() == "+" || peek() == "-" else { return true } let negative = consume("-") - if !negative { advance() } + if !negative { + advance() + } guard let hour = readFixed(2), consume(":"), let minute = readFixed(2) else { return false } guard (0 ... 14).contains(hour), (0 ... 59).contains(minute), hour * 60 + minute <= 14 * 60 else { return false @@ -136,7 +141,9 @@ extension PureXML.Schema { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } private mutating func consume(_ literal: String) -> Bool { diff --git a/Sources/Schema/DecimalValue.swift b/Sources/Schema/DecimalValue.swift index 0299ef3..d953810 100644 --- a/Sources/Schema/DecimalValue.swift +++ b/Sources/Schema/DecimalValue.swift @@ -65,7 +65,9 @@ extension PureXML.Schema { } static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool { - if lhs.negative != rhs.negative { return lhs.negative } + if lhs.negative != rhs.negative { + return lhs.negative + } let magnitude = compareMagnitude(lhs, rhs) return lhs.negative ? magnitude > 0 : magnitude < 0 } @@ -75,7 +77,9 @@ extension PureXML.Schema { if lhs.integerDigits.count != rhs.integerDigits.count { return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1 } - if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) { return order } + if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) { + return order + } return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0 } @@ -85,7 +89,9 @@ extension PureXML.Schema { for (left, right) in zip(lhs, rhs) where left != right { return left < right ? -1 : 1 } - if lhs.count == rhs.count { return nil } + if lhs.count == rhs.count { + return nil + } return lhs.count < rhs.count ? -1 : 1 } } diff --git a/Sources/Schema/DurationValue.swift b/Sources/Schema/DurationValue.swift index adfaf99..ffe4802 100644 --- a/Sources/Schema/DurationValue.swift +++ b/Sources/Schema/DurationValue.swift @@ -36,9 +36,15 @@ extension PureXML.Schema { } // P < Q only when s+P < s+Q at every reference; > likewise; equal // when all four coincide (which holds iff the components are equal). - if sawLess, !sawGreater, !sawEqual { return .lessThan } - if sawGreater, !sawLess, !sawEqual { return .greaterThan } - if sawEqual, !sawLess, !sawGreater { return .equal } + if sawLess, !sawGreater, !sawEqual { + return .lessThan + } + if sawGreater, !sawLess, !sawEqual { + return .greaterThan + } + if sawEqual, !sawLess, !sawGreater { + return .equal + } return .incomparable } diff --git a/Sources/Schema/IDTracker.swift b/Sources/Schema/IDTracker.swift index 90e6e5e..3494da2 100644 --- a/Sources/Schema/IDTracker.swift +++ b/Sources/Schema/IDTracker.swift @@ -35,19 +35,25 @@ extension PureXML.Schema { extension PureXML.Schema.SimpleType { /// Whether this is `xs:ID` (or an atomic restriction of it). var isID: Bool { - if case .atomic = variety { return base == .id } + if case .atomic = variety { + return base == .id + } return false } /// Whether this is `xs:IDREF` (or an atomic restriction of it). var isIDReference: Bool { - if case .atomic = variety { return base == .idref } + if case .atomic = variety { + return base == .idref + } return false } /// Whether this is `xs:IDREFS` (a list whose item type is `xs:IDREF`). var isIDReferenceList: Bool { - if case let .list(item) = variety { return item.base == .idref } + if case let .list(item) = variety { + return item.base == .idref + } return false } } diff --git a/Sources/Schema/IdentityFieldSimplicity.swift b/Sources/Schema/IdentityFieldSimplicity.swift index 4486286..ef46374 100644 --- a/Sources/Schema/IdentityFieldSimplicity.swift +++ b/Sources/Schema/IdentityFieldSimplicity.swift @@ -38,7 +38,9 @@ extension PureXML.Schema.XSDParser { var current: XSDTree? = start for rawStep in path.split(separator: "/", omittingEmptySubsequences: false) { let step = String(rawStep).trimmingXMLWhitespace() - if step == "." { continue } + if step == "." { + continue + } guard isSimpleNameTest(step), let node = current, let child = directChildElement(named: PureXML.Schema.XSDNode.stripPrefix(step), of: node) else { return nil } @@ -92,7 +94,9 @@ extension PureXML.Schema.XSDParser { } return local == "anyType" } - if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil { return false } + if PureXML.Schema.XSDNode.firstChild(element, named: "simpleType") != nil { + return false + } if let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") { return PureXML.Schema.XSDNode.firstChild(complex, named: "simpleContent") == nil } diff --git a/Sources/Schema/IdentityFieldType.swift b/Sources/Schema/IdentityFieldType.swift index 10b1ec4..d45a3f7 100644 --- a/Sources/Schema/IdentityFieldType.swift +++ b/Sources/Schema/IdentityFieldType.swift @@ -13,7 +13,9 @@ extension PureXML.Schema.IdentityValidator { let fieldType = childElementType(named: field, in: targetComplex.content), case let .complex(fieldComplex) = fieldType else { return false } - if case .simpleContent = fieldComplex.content { return false } + if case .simpleContent = fieldComplex.content { + return false + } return true } diff --git a/Sources/Schema/IdentityTupleSet.swift b/Sources/Schema/IdentityTupleSet.swift index 36cd0cb..cec44b2 100644 --- a/Sources/Schema/IdentityTupleSet.swift +++ b/Sources/Schema/IdentityTupleSet.swift @@ -49,7 +49,9 @@ struct TupleSet { key.reserveCapacity(tuple.count) for field in tuple { guard let field, !field.string.contains(":") else { return nil } - if let type = field.type, !type.comparesRawLexically { return nil } + if let type = field.type, !type.comparesRawLexically { + return nil + } key.append(field.string) } return key diff --git a/Sources/Schema/IdentityValidator.swift b/Sources/Schema/IdentityValidator.swift index e37a392..551d019 100644 --- a/Sources/Schema/IdentityValidator.swift +++ b/Sources/Schema/IdentityValidator.swift @@ -49,11 +49,22 @@ extension PureXML.Schema { _ root: PureXML.Model.TreeNode, at path: [PureXML.Validation.PathKey] = [], ) -> [PureXML.Validation.ValidationError] { + validateTagged(root, at: path).map(\.error) + } + + /// The tagged form of ``validate(_:at:)``: the same computation, with each + /// finding labeled by the decomposed identity rule it belongs to. The + /// public, untagged form is a thin `.map(\.error)` over this so behavior and + /// output stay identical for every existing caller. + func validateTagged( + _ root: PureXML.Model.TreeNode, + at path: [PureXML.Validation.PathKey] = [], + ) -> [XSDFinding] { guard !constraints.isEmpty else { return [] } // A selector or field XPath that does not compile is a schema-author // error, reported once up front rather than silently disabling the // constraint (the broken query still evaluates as no-match below). - var issues: [PureXML.Validation.ValidationError] = compileErrors(at: path) + var issues: [XSDFinding] = compileErrors(at: path) walk(root, at: path, scopes: [], into: &issues) return issues } @@ -61,15 +72,19 @@ extension PureXML.Schema { /// One located error per constraint XPath that fails to compile, in a /// deterministic order. The failed compilations are cached, so the walk /// afterwards treats those constraints as matching nothing. - private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [PureXML.Validation.ValidationError] { - var errors: [PureXML.Validation.ValidationError] = [] + private func compileErrors(at path: [PureXML.Validation.PathKey]) -> [XSDFinding] { + var errors: [XSDFinding] = [] for key in constraints.keys.sorted() { for constraint in constraints[key] ?? [] { if cache.query(constraint.selector) == nil { - errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'", at: path)) + errors.append(XSDFinding( + .identityConstraintXPathsValid, + reason: "identity constraint '\(constraint.name)': invalid selector XPath '\(constraint.selector)'", + at: path, + )) } for field in constraint.fields where cache.query(field) == nil { - errors.append(.init(reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path)) + errors.append(XSDFinding(.identityConstraintXPathsValid, reason: "identity constraint '\(constraint.name)': invalid field XPath '\(field)'", at: path)) } } } @@ -80,7 +95,7 @@ extension PureXML.Schema { _ node: PureXML.Model.TreeNode, at path: [PureXML.Validation.PathKey], scopes: [[String: [[FieldValue?]]]], - into issues: inout [PureXML.Validation.ValidationError], + into issues: inout [XSDFinding], ) { var frame: [String: [[FieldValue?]]] = [:] let declared = node.name.flatMap { constraints[$0.localName] } ?? [] @@ -132,7 +147,7 @@ extension PureXML.Schema { at node: PureXML.Model.TreeNode, path: [PureXML.Validation.PathKey], frame: inout [String: [[FieldValue?]]], - into issues: inout [PureXML.Validation.ValidationError], + into issues: inout [XSDFinding], ) { guard isNonRef(constraint) else { return } var tuples: [[FieldValue?]] = [] @@ -145,15 +160,19 @@ extension PureXML.Schema { } let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint) if constraint.kind == .key, tuple.contains(where: { $0 == nil }) { - issues.append(.init( + issues.append(XSDFinding( + .identityKeyFieldPresent, reason: "key '\(constraint.name)': a field is missing", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) continue } - if tuple.contains(where: { $0 == nil }) { continue } + if tuple.contains(where: { $0 == nil }) { + continue + } if seen.contains(tuple) { - issues.append(.init( + issues.append(XSDFinding( + .identityValueUnique, reason: "\(label(constraint)) '\(constraint.name)': duplicate value", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) @@ -170,7 +189,7 @@ extension PureXML.Schema { at node: PureXML.Model.TreeNode, path: [PureXML.Validation.PathKey], scopes: [[String: [[FieldValue?]]]], - into issues: inout [PureXML.Validation.ValidationError], + into issues: inout [XSDFinding], ) { guard case let .keyref(refer) = constraint.kind else { return } let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? [] @@ -185,9 +204,12 @@ extension PureXML.Schema { continue } let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint) - if tuple.contains(where: { $0 == nil }) { continue } + if tuple.contains(where: { $0 == nil }) { + continue + } if !keys.contains(tuple) { - issues.append(.init( + issues.append(XSDFinding( + .identityKeyrefResolves, reason: "keyref '\(constraint.name)': no matching key '\(refer)'", at: targetPath + fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings), )) @@ -203,13 +225,14 @@ extension PureXML.Schema { _ constraint: IdentityConstraint, at target: PureXML.Model.TreeNode, path targetPath: [PureXML.Validation.PathKey], - ) -> PureXML.Validation.ValidationError? { + ) -> XSDFinding? { let step = fieldStep(constraint.fields, at: target, namespaces: constraint.namespaceBindings) for field in constraint.fields { let selected = select(field, at: target, namespaces: constraint.namespaceBindings) // cvc-identity-constraint.3: each field must evaluate to at most one node. if selected.count > 1 { - return .init( + return XSDFinding( + .identityFieldCardinality, reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must select at most one node", at: targetPath + step, ) @@ -235,7 +258,8 @@ extension PureXML.Schema { && Self.xsiTypeLocalName(of: node) == nil } ?? false if element != nil, hasElementChildren || declaredNonSimple || fieldTargetIsComplexNonSimple(field, at: target) { - return .init( + return XSDFinding( + .identityFieldMustBeSimple, reason: "\(label(constraint)) '\(constraint.name)': field '\(field)' must identify a node with simple content", at: targetPath + step, ) @@ -270,7 +294,9 @@ extension PureXML.Schema { if let defaulted = defaultedValue(field, value: value, type: type, constraint: constraint, at: node) { return defaulted } - if let nodes = value.nodes, nodes.isEmpty { return nil } + if let nodes = value.nodes, nodes.isEmpty { + return nil + } return FieldValue(string: value.string, type: type ?? Self.effectiveType(of: value), namespaceBindings: instanceNamespaces) } } @@ -319,69 +345,3 @@ extension PureXML.Schema { } } } - -extension PureXML.Schema.IdentityValidator { - func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? { - switch declaration { - case let .simple(simple): simple - case let .complex(complex): - if case let .simpleContent(simple) = complex.content { simple } else { nil } - case .typeReference: nil - } - } - - static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? { - guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first, - treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode), - let builtin = PureXML.Schema.BuiltinType(rawValue: local) - else { return nil } - return PureXML.Schema.SimpleType(base: builtin) - } - - /// The local name of a tree node's `xsi:type` attribute value, recognized by - /// the schema-instance namespace or the conventional `xsi` prefix, or nil. - static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? { - let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance" - guard let attribute = node.attributes.first(where: { - $0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi") - }) else { return nil } - return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value - } - - func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String { - constraint.kind == .key ? "key" : "unique" - } - - /// Whether a constraint collects tuples (`key`/`unique`) rather than - /// referencing them (`keyref`). - func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool { - if case .keyref = constraint.kind { return false } - return true - } - - /// The coding-path steps from the constraint-bearing `container` down to a - /// selected `target`, so an identity error locates the actual offending - /// element rather than the element that merely declares the constraint. - /// Returns an empty path if `target` is not a descendant of `container`. - func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] { - var chain: [PureXML.Model.TreeNode] = [] - var current = target - while current !== container { - guard let parent = current.parent else { return [] } - chain.append(current) - current = parent - } - var steps: [PureXML.Validation.PathKey] = [] - var parent = container - for node in chain.reversed() { - let name = node.name?.description ?? "" - if let index = stepIndex.position(of: node, under: parent, name: name) { - steps.append(.element(name, index: index)) - } else { - steps.append(.element(name)) - } - parent = node - } - return steps - } -} diff --git a/Sources/Schema/IdentityValidatorDefaults.swift b/Sources/Schema/IdentityValidatorDefaults.swift index 24bec62..dce41cf 100644 --- a/Sources/Schema/IdentityValidatorDefaults.swift +++ b/Sources/Schema/IdentityValidatorDefaults.swift @@ -23,7 +23,9 @@ extension PureXML.Schema.IdentityValidator { /// character content; any other field is empty only on an empty node-set. func isFieldEmpty(_ field: String, value: PureXML.XPath.Value) -> Bool { guard let nodes = value.nodes else { return false } - if field.hasPrefix("@") { return nodes.isEmpty } + if field.hasPrefix("@") { + return nodes.isEmpty + } if field == "." { guard case let .tree(element)? = nodes.first, element.kind == .element else { return nodes.isEmpty } return !element.children.contains { $0.kind == .text || $0.kind == .cdata } @@ -41,7 +43,9 @@ extension PureXML.Schema.IdentityValidator { ) -> PureXML.Schema.ValueConstraint? { if field == ".", let local = target.name?.localName { let key = PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local) - if let value = fieldConstraints[key] { return value } + if let value = fieldConstraints[key] { + return value + } } return fieldConstraints[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)] } diff --git a/Sources/Schema/IdentityValidatorHelpers.swift b/Sources/Schema/IdentityValidatorHelpers.swift new file mode 100644 index 0000000..c037b76 --- /dev/null +++ b/Sources/Schema/IdentityValidatorHelpers.swift @@ -0,0 +1,71 @@ +extension PureXML.Schema.IdentityValidator { + func simpleType(of declaration: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? { + switch declaration { + case let .simple(simple): simple + case let .complex(complex): + if case let .simpleContent(simple) = complex.content { + simple + } else { + nil + } + case .typeReference: nil + } + } + + static func effectiveType(of value: PureXML.XPath.Value) -> PureXML.Schema.SimpleType? { + guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first, + treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode), + let builtin = PureXML.Schema.BuiltinType(rawValue: local) + else { return nil } + return PureXML.Schema.SimpleType(base: builtin) + } + + /// The local name of a tree node's `xsi:type` attribute value, recognized by + /// the schema-instance namespace or the conventional `xsi` prefix, or nil. + static func xsiTypeLocalName(of node: PureXML.Model.TreeNode) -> String? { + let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + guard let attribute = node.attributes.first(where: { + $0.name.localName == "type" && ($0.name.namespaceURI == schemaInstance || $0.name.prefix == "xsi") + }) else { return nil } + return attribute.value.split(separator: ":").last.map(String.init) ?? attribute.value + } + + func label(_ constraint: PureXML.Schema.IdentityConstraint) -> String { + constraint.kind == .key ? "key" : "unique" + } + + /// Whether a constraint collects tuples (`key`/`unique`) rather than + /// referencing them (`keyref`). + func isNonRef(_ constraint: PureXML.Schema.IdentityConstraint) -> Bool { + if case .keyref = constraint.kind { + return false + } + return true + } + + /// The coding-path steps from the constraint-bearing `container` down to a + /// selected `target`, so an identity error locates the actual offending + /// element rather than the element that merely declares the constraint. + /// Returns an empty path if `target` is not a descendant of `container`. + func steps(from container: PureXML.Model.TreeNode, to target: PureXML.Model.TreeNode) -> [PureXML.Validation.PathKey] { + var chain: [PureXML.Model.TreeNode] = [] + var current = target + while current !== container { + guard let parent = current.parent else { return [] } + chain.append(current) + current = parent + } + var steps: [PureXML.Validation.PathKey] = [] + var parent = container + for node in chain.reversed() { + let name = node.name?.description ?? "" + if let index = stepIndex.position(of: node, under: parent, name: name) { + steps.append(.element(name, index: index)) + } else { + steps.append(.element(name)) + } + parent = node + } + return steps + } +} diff --git a/Sources/Schema/Lexical.swift b/Sources/Schema/Lexical.swift index 8c1ce1f..0e23fe3 100644 --- a/Sources/Schema/Lexical.swift +++ b/Sources/Schema/Lexical.swift @@ -9,9 +9,13 @@ extension PureXML.Schema { /// `float`/`double`: an optional-sign mantissa with an optional exponent, /// or the special values `INF`, `-INF`, `NaN`. static func isFloating(_ value: String) -> Bool { - if value == "INF" || value == "-INF" || value == "NaN" { return true } + if value == "INF" || value == "-INF" || value == "NaN" { + return true + } var characters = Substring(value) - if characters.first == "+" || characters.first == "-" { characters = characters.dropFirst() } + if characters.first == "+" || characters.first == "-" { + characters = characters.dropFirst() + } let exponentSplit = characters.split(separator: "e", omittingEmptySubsequences: false) let altSplit = characters.split(separator: "E", omittingEmptySubsequences: false) let parts = exponentSplit.count == 2 ? exponentSplit : altSplit @@ -31,7 +35,9 @@ extension PureXML.Schema { private static func isSignedInteger(_ value: Substring) -> Bool { var digits = value - if digits.first == "+" || digits.first == "-" { digits = digits.dropFirst() } + if digits.first == "+" || digits.first == "-" { + digits = digits.dropFirst() + } return !digits.isEmpty && digits.allSatisfy(\.isNumber) } @@ -39,7 +45,9 @@ extension PureXML.Schema { /// least one component and no empty `T`. static func isDuration(_ value: String) -> Bool { var rest = Substring(value) - if rest.first == "-" { rest = rest.dropFirst() } + if rest.first == "-" { + rest = rest.dropFirst() + } guard rest.first == "P" else { return false } rest = rest.dropFirst() let timeSplit = rest.split(separator: "T", omittingEmptySubsequences: false) diff --git a/Sources/Schema/OccurrenceRange.swift b/Sources/Schema/OccurrenceRange.swift index 1b7d26b..e8c2729 100644 --- a/Sources/Schema/OccurrenceRange.swift +++ b/Sources/Schema/OccurrenceRange.swift @@ -14,7 +14,9 @@ public extension PureXML.Schema { public init?(lexical value: String) { var raw = Substring(value.trimmingXMLWhitespace()) - if raw.first == "+" { raw = raw.dropFirst() } + if raw.first == "+" { + raw = raw.dropFirst() + } guard !raw.isEmpty, raw.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil } @@ -32,14 +34,18 @@ public extension PureXML.Schema { func clamped(to limit: Int) -> Int { precondition(limit >= 0, "occurrence clamp limit must be non-negative") - if isGreaterThan(limit) { return limit } + if isGreaterThan(limit) { + return limit + } return Int(digits) ?? limit } func isGreaterThan(_ value: Int) -> Bool { precondition(value >= 0, "occurrence comparison value must be non-negative") let other = String(value) - if digits.count != other.count { return digits.count > other.count } + if digits.count != other.count { + return digits.count > other.count + } return digits > other } @@ -69,7 +75,9 @@ public extension PureXML.Schema { } public var isZero: Bool { - if case let .finite(value) = self { return value.isZero } + if case let .finite(value) = self { + return value.isZero + } return false } diff --git a/Sources/Schema/ParticleNormalization.swift b/Sources/Schema/ParticleNormalization.swift index c33bf9f..4841709 100644 --- a/Sources/Schema/ParticleNormalization.swift +++ b/Sources/Schema/ParticleNormalization.swift @@ -9,7 +9,9 @@ extension PureXML.Schema.Particle { var members: [Self] = [] for child in group.particles { let normalized = child.normalized() - if normalized.maxOccurs == 0 { continue } + if normalized.maxOccurs == 0 { + continue + } let splice = normalized.minOccurs == 1 && normalized.maxOccurs == 1 if case let .group(inner) = normalized.term, inner.compositor == group.compositor, splice { members.append(contentsOf: inner.particles) diff --git a/Sources/Schema/ParticleRestriction.swift b/Sources/Schema/ParticleRestriction.swift index bb20d96..72622bf 100644 --- a/Sources/Schema/ParticleRestriction.swift +++ b/Sources/Schema/ParticleRestriction.swift @@ -187,7 +187,9 @@ extension PureXML.Schema { /// has `maxOccurs=0`, or is a group all of whose members are content-free /// (an empty group included). Its only legal content is the empty sequence. static func contentFree(_ particle: Particle) -> Bool { - if particle.maxOccurs == 0 { return true } + if particle.maxOccurs == 0 { + return true + } switch particle.term { case .element, .wildcard: return false @@ -216,13 +218,19 @@ extension PureXML.Schema { matched = true break } - if skippedMustBeEmptiable, !emptiable(candidate) { return false } + if skippedMustBeEmptiable, !emptiable(candidate) { + return false + } + } + if !matched { + return false } - if !matched { return false } } if skippedMustBeEmptiable { while baseIndex < base.count { - if !emptiable(base[baseIndex]) { return false } + if !emptiable(base[baseIndex]) { + return false + } baseIndex += 1 } } @@ -253,7 +261,9 @@ extension PureXML.Schema { /// Whether a particle can match no children at all. static func emptiable(_ particle: Particle) -> Bool { - if particle.minOccurs == 0 { return true } + if particle.minOccurs == 0 { + return true + } switch particle.term { case .element, .wildcard: return false diff --git a/Sources/Schema/ParticleRestrictionElement.swift b/Sources/Schema/ParticleRestrictionElement.swift index 8a96b39..0579b39 100644 --- a/Sources/Schema/ParticleRestrictionElement.swift +++ b/Sources/Schema/ParticleRestrictionElement.swift @@ -50,7 +50,9 @@ extension PureXML.Schema.ParticleRestriction { // declared union whose members merely subset the base union's is // NOT a derivation (particlesIk011); structural member subsetting // is not the spec's derivation relation. - if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { return true } + if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { + return true + } return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) } } case (.complex, .simple): @@ -62,7 +64,9 @@ extension PureXML.Schema.ParticleRestriction { } } - if baseIsUnion(baseTypeName, types) { return true } + if baseIsUnion(baseTypeName, types) { + return true + } // NameAndTypeOK.2.2: a restricting element's type must be validly derived from // the base element's type EXCLUDING extension (and list/union), i.e. by // restriction only. A type that reaches the base through an extension step is @@ -79,7 +83,9 @@ extension PureXML.Schema.ParticleRestriction { while let resolved = current, steps <= types.count { switch resolved { case let .simple(simple): - if case .union = simple.variety { return true } + if case .union = simple.variety { + return true + } return false case .complex: return false @@ -112,11 +118,15 @@ extension PureXML.Schema.ParticleRestriction { _ derivation: [String: PureXML.Schema.TypeDerivation], _ types: [String: PureXML.Schema.ElementType], ) -> Bool { - if isUrTypeName(base, types) { return true } + if isUrTypeName(base, types) { + return true + } var current = derived var visited: Set = [] while visited.insert(current).inserted { - if current == base { return true } + if current == base { + return true + } guard let step = derivation[current] else { break } current = step.base } @@ -139,11 +149,15 @@ extension PureXML.Schema.ParticleRestriction { _ derivation: [String: PureXML.Schema.TypeDerivation], _ types: [String: PureXML.Schema.ElementType], ) -> Bool { - if isUrTypeName(base, types) { return true } + if isUrTypeName(base, types) { + return true + } var current = derived var visited: Set = [] while visited.insert(current).inserted { - if current == base { return true } + if current == base { + return true + } guard let step = derivation[current], step.method == .restriction else { break } current = step.base } diff --git a/Sources/Schema/RelaxNGCompactHelpers.swift b/Sources/Schema/RelaxNGCompactHelpers.swift index 878d820..87b46d6 100644 --- a/Sources/Schema/RelaxNGCompactHelpers.swift +++ b/Sources/Schema/RelaxNGCompactHelpers.swift @@ -58,7 +58,9 @@ extension RNCParser { private func qualifiedName(_ word: String, attribute: Bool) -> NameClass { let parts = word.split(separator: ":", maxSplits: 1).map(String.init) if parts.count == 2 { - if parts[1] == "*" { return .nsName(namespaces[parts[0]] ?? "") } + if parts[1] == "*" { + return .nsName(namespaces[parts[0]] ?? "") + } return .name(namespace: namespaces[parts[0]] ?? "", localName: parts[1]) } return .name(namespace: attribute ? "" : defaultNamespace, localName: word) @@ -78,7 +80,8 @@ extension RNCParser { } func wordValue() -> String { - if case let .word(word) = peek() { advance() + if case let .word(word) = peek() { + advance() return word } return "" @@ -93,11 +96,14 @@ extension RNCParser { } func expectSymbol(_ symbol: String) { - if peek() == .symbol(symbol) { advance() } + if peek() == .symbol(symbol) { + advance() + } } func expectString() -> String { - if case let .string(value) = peek() { advance() + if case let .string(value) = peek() { + advance() return concatenated(value) } return "" diff --git a/Sources/Schema/RelaxNGCompactParser.swift b/Sources/Schema/RelaxNGCompactParser.swift index c2972ff..b9fe836 100644 --- a/Sources/Schema/RelaxNGCompactParser.swift +++ b/Sources/Schema/RelaxNGCompactParser.swift @@ -50,24 +50,33 @@ extension RNCParser { private func parseDefault() { advance() // default - if case .word("namespace") = peek() { parseNamespace(isDefault: true) } + if case .word("namespace") = peek() { + parseNamespace(isDefault: true) + } } private func parseNamespace(isDefault: Bool) { advance() // namespace var prefix = "" - if case let .word(name) = peek(), name != "=" { prefix = name + if case let .word(name) = peek(), name != "=" { + prefix = name advance() } expectSymbol("=") let uri = expectString() - if isDefault { defaultNamespace = uri } - if !prefix.isEmpty { namespaces[prefix] = uri } + if isDefault { + defaultNamespace = uri + } + if !prefix.isEmpty { + namespaces[prefix] = uri + } } private func parseDatatypes() { advance() // datatypes - if case .word = peek() { advance() } + if case .word = peek() { + advance() + } expectSymbol("=") _ = expectString() } @@ -76,9 +85,13 @@ extension RNCParser { private func looksLikeGrammar() -> Bool { guard case let .word(word) = peek() else { return false } - if word == "include" || word == "start" { return true } + if word == "include" || word == "start" { + return true + } // A define is a word followed by one of =, |=, &=. - if case let .symbol(symbol) = peek(1), symbol == "=" || symbol == "|=" || symbol == "&=" { return true } + if case let .symbol(symbol) = peek(1), symbol == "=" || symbol == "|=" || symbol == "&=" { + return true + } return false } @@ -118,7 +131,11 @@ extension RNCParser { var depth = 1 while position < tokens.count, depth > 0 { let token = advance() - if token == .symbol("[") { depth += 1 } else if token == .symbol("]") { depth -= 1 } + if token == .symbol("[") { + depth += 1 + } else if token == .symbol("]") { + depth -= 1 + } } } } @@ -148,7 +165,9 @@ extension RNCParser { for (name, pattern) in sub.defines { defines[name] = pattern } - if startPattern == nil { startPattern = sub.startPattern } + if startPattern == nil { + startPattern = sub.startPattern + } visited = sub.visited } // An override block re-defines names after the include, overriding them. @@ -212,7 +231,9 @@ extension RNCParser { while peek() == .symbol(">"), peek(1) == .symbol(">") { advance() advance() - if case .word = peek() { advance() } + if case .word = peek() { + advance() + } skipAnnotation() } } @@ -281,8 +302,11 @@ extension RNCParser { guard word.contains(":") else { return .ref(word) } let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string var facets = PureXML.Schema.Facets() - if peek() == .symbol("{") { parseParams(into: &facets) } - if case let .string(value) = peek() { advance() + if peek() == .symbol("{") { + parseParams(into: &facets) + } + if case let .string(value) = peek() { + advance() return .value(PureXML.Schema.SimpleType(base: type), concatenated(value)) } return .data(PureXML.Schema.SimpleType(base: type, facets: facets)) @@ -297,12 +321,16 @@ extension RNCParser { continue } advance() - if peek() == .symbol("=") { advance() } + if peek() == .symbol("=") { + advance() + } guard case let .string(value) = peek() else { continue } advance() PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets) } - if peek() == .symbol("}") { advance() } + if peek() == .symbol("}") { + advance() + } } private func parseElement() -> Pattern { diff --git a/Sources/Schema/RelaxNGDerivative.swift b/Sources/Schema/RelaxNGDerivative.swift index ad06f03..30d39b2 100644 --- a/Sources/Schema/RelaxNGDerivative.swift +++ b/Sources/Schema/RelaxNGDerivative.swift @@ -47,30 +47,54 @@ extension PureXML.Schema { // MARK: Simplifying constructors func choice(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { - if case .notAllowed = lhs { return rhs } - if case .notAllowed = rhs { return lhs } + if case .notAllowed = lhs { + return rhs + } + if case .notAllowed = rhs { + return lhs + } return .choice(lhs, rhs) } private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { - if case .notAllowed = lhs { return .notAllowed } - if case .notAllowed = rhs { return .notAllowed } - if case .empty = lhs { return rhs } - if case .empty = rhs { return lhs } + if case .notAllowed = lhs { + return .notAllowed + } + if case .notAllowed = rhs { + return .notAllowed + } + if case .empty = lhs { + return rhs + } + if case .empty = rhs { + return lhs + } return .group(lhs, rhs) } private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { - if case .notAllowed = lhs { return .notAllowed } - if case .notAllowed = rhs { return .notAllowed } - if case .empty = lhs { return rhs } - if case .empty = rhs { return lhs } + if case .notAllowed = lhs { + return .notAllowed + } + if case .notAllowed = rhs { + return .notAllowed + } + if case .empty = lhs { + return rhs + } + if case .empty = rhs { + return lhs + } return .interleave(lhs, rhs) } private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { - if case .notAllowed = lhs { return .notAllowed } - if case .notAllowed = rhs { return .notAllowed } + if case .notAllowed = lhs { + return .notAllowed + } + if case .notAllowed = rhs { + return .notAllowed + } return .after(lhs, rhs) } @@ -155,7 +179,9 @@ extension PureXML.Schema { } private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String, env: [String: String]) -> Bool { - if nullable(textDeriv(content, value, env: env)) { return true } + if nullable(textDeriv(content, value, env: env)) { + return true + } // A whitespace-only attribute value also matches a pattern that // matches the empty sequence, the same leniency element content // gets for ignorable whitespace. @@ -163,7 +189,9 @@ extension PureXML.Schema { } private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { - if case .notAllowed = inner { return .notAllowed } + if case .notAllowed = inner { + return .notAllowed + } return .oneOrMore(inner) } } @@ -244,7 +272,9 @@ extension PureXML.Schema.RelaxNGEngine { let coalesced = Self.coalesceText(children) // Clark's algorithm treats an empty element as optional empty text, so // a data/value pattern whose lexical space admits "" matches . - if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "", env: env)) } + if coalesced.isEmpty { + return choice(pattern, textDeriv(pattern, "", env: env)) + } if coalesced.count == 1, case let .text(string) = coalesced[0] { let derived = textDeriv(pattern, string, env: env) return Self.isWhitespace(string) ? choice(pattern, derived) : derived @@ -276,13 +306,16 @@ extension PureXML.Schema.RelaxNGEngine { case .comment, .processingInstruction: continue default: - if !run.isEmpty { result.append(.text(run)) + if !run.isEmpty { + result.append(.text(run)) run = "" } result.append(child) } } - if !run.isEmpty { result.append(.text(run)) } + if !run.isEmpty { + result.append(.text(run)) + } return result } @@ -291,7 +324,9 @@ extension PureXML.Schema.RelaxNGEngine { } static func isWhitespaceText(_ node: PureXML.Model.Node) -> Bool { - if case let .text(value) = node { return isWhitespace(value) } + if case let .text(value) = node { + return isWhitespace(value) + } return false } @@ -341,7 +376,9 @@ extension PureXML.Schema.RelaxNGEngine { switch event { case let .startElement(name, attributes): flushText(&state, closing: false) - if !state.frames.isEmpty { state.frames[state.frames.count - 1].hadElementChild = true } + if !state.frames.isEmpty { + state.frames[state.frames.count - 1].hadElementChild = true + } var env = state.frames.last?.env ?? Self.rootEnvironment for attribute in attributes where Self.isNamespaceDeclaration(attribute) { env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value @@ -353,11 +390,15 @@ extension PureXML.Schema.RelaxNGEngine { state.current = startTagCloseDeriv(attributed) state.frames.append(RelaxNGStreamFrame(env: env)) case let .characters(text), let .cdata(text): - if !state.frames.isEmpty { state.frames[state.frames.count - 1].pendingText += text } + if !state.frames.isEmpty { + state.frames[state.frames.count - 1].pendingText += text + } case .endElement: flushText(&state, closing: true) state.current = endTagDeriv(state.current) - if !state.frames.isEmpty { state.frames.removeLast() } + if !state.frames.isEmpty { + state.frames.removeLast() + } case .comment, .processingInstruction: break } diff --git a/Sources/Schema/RelaxNGDiagnostics.swift b/Sources/Schema/RelaxNGDiagnostics.swift index fbabd61..e365dd0 100644 --- a/Sources/Schema/RelaxNGDiagnostics.swift +++ b/Sources/Schema/RelaxNGDiagnostics.swift @@ -18,12 +18,16 @@ extension PureXML.Schema.RelaxNGEngine { } else { validateElement(opened, root, path, &errors) } - if errors.isEmpty { errors.append(.init(reason: "does not satisfy the RELAX NG schema", at: path)) } + if errors.isEmpty { + errors.append(.init(reason: "does not satisfy the RELAX NG schema", at: path)) + } return errors } private func isNotAllowed(_ pattern: Pattern) -> Bool { - if case .notAllowed = pattern { return true } + if case .notAllowed = pattern { + return true + } return false } @@ -46,7 +50,9 @@ extension PureXML.Schema.RelaxNGEngine { if isNotAllowed(closed) { // A genuine missing/extra attribute, unless an attribute error above // already explains why the requirement is unsatisfied (no cascade). - if errors.count == before { errors.append(.init(reason: "element <\(name)> is missing a required attribute", at: path)) } + if errors.count == before { + errors.append(.init(reason: "element <\(name)> is missing a required attribute", at: path)) + } return } // An attribute problem leaves the content model unreliable; stop here so a diff --git a/Sources/Schema/RelaxNGGrammarCheck.swift b/Sources/Schema/RelaxNGGrammarCheck.swift index ea65eff..bff9691 100644 --- a/Sources/Schema/RelaxNGGrammarCheck.swift +++ b/Sources/Schema/RelaxNGGrammarCheck.swift @@ -23,8 +23,12 @@ enum RelaxNGGrammarCheck { static func check(_ node: RNGTree, in context: Context) -> String? { guard let name = RNGNode.localName(node) else { return nil } - if let problem = attributeProblem(node, name: name) { return problem } - if let problem = textProblem(node, name: name) { return problem } + if let problem = attributeProblem(node, name: name) { + return problem + } + if let problem = textProblem(node, name: name) { + return problem + } switch context { case .pattern: return checkPattern(node, name: name) case .nameClass: return checkNameClass(node, name: name) @@ -102,7 +106,9 @@ enum RelaxNGGrammarCheck { } } else { guard let nameClass = content.first else { return "<\(name)> needs a name class" } - if let problem = check(nameClass, in: .nameClass) { return problem } + if let problem = check(nameClass, in: .nameClass) { + return problem + } content = content.dropFirst() } if name == "element", content.isEmpty { @@ -160,14 +166,20 @@ enum RelaxNGGrammarCheck { for child in children { switch RNGNode.localName(child) { case "param": - if sawExcept { return " may not follow in " } + if sawExcept { + return " may not follow in " + } guard hasNCName(child, "name") else { return " needs a name attribute" } case "except": - if sawExcept { return " takes at most one " } + if sawExcept { + return " takes at most one " + } sawExcept = true let members = RNGNode.elementChildren(child) guard !members.isEmpty else { return " in needs content" } - if let problem = firstViolation(members, in: .dataExcept) { return problem } + if let problem = firstViolation(members, in: .dataExcept) { + return problem + } default: return "<\(RNGNode.localName(child) ?? "?")> is not allowed in " } diff --git a/Sources/Schema/RelaxNGGrammarCheckSections.swift b/Sources/Schema/RelaxNGGrammarCheckSections.swift index 53ebf9a..a0b3796 100644 --- a/Sources/Schema/RelaxNGGrammarCheckSections.swift +++ b/Sources/Schema/RelaxNGGrammarCheckSections.swift @@ -34,7 +34,9 @@ extension RelaxNGGrammarCheck { return "<\(name)> admits only an child" } } - if children.count > 1 { return "<\(name)> takes at most one " } + if children.count > 1 { + return "<\(name)> takes at most one " + } guard let except = children.first else { return nil } let members = RNGNode.elementChildren(except) guard !members.isEmpty else { return " needs content" } @@ -61,7 +63,9 @@ extension RelaxNGGrammarCheck { /// `start` takes exactly one pattern; `define` needs a name and at least /// one pattern; both may carry a combine method. private static func checkCombiningComponent(_ node: RNGTree, name: String, children: [RNGTree]) -> String? { - if let problem = combineProblem(node) { return problem } + if let problem = combineProblem(node) { + return problem + } if name == "start" { guard children.count == 1 else { return " takes exactly one pattern" } return check(children[0], in: .pattern) @@ -82,7 +86,9 @@ extension RelaxNGGrammarCheck { static func firstViolation(_ nodes: [RNGTree], in context: Context) -> String? { for node in nodes { - if let problem = check(node, in: context) { return problem } + if let problem = check(node, in: context) { + return problem + } } return nil } @@ -120,7 +126,9 @@ extension RelaxNGGrammarCheck { /// of ALPHA *(ALPHA / DIGIT / "+" / "-" / "."), or the empty string for /// the built-in library. static func isDatatypeLibraryURI(_ value: String) -> Bool { - if value.isEmpty { return true } + if value.isEmpty { + return true + } guard !value.contains("#") else { return false } // A percent must introduce a two-digit hex escape (RFC 2396). var cursor = value.startIndex diff --git a/Sources/Schema/RelaxNGNode.swift b/Sources/Schema/RelaxNGNode.swift index f098117..8e2686b 100644 --- a/Sources/Schema/RelaxNGNode.swift +++ b/Sources/Schema/RelaxNGNode.swift @@ -27,7 +27,9 @@ enum RNGNode { // only when the whole document is unqualified (leniency for // namespace-less schemas); under a qualified parent it is foreign. let uri = child.name?.namespaceURI - if uri == relaxNGNamespace { return true } + if uri == relaxNGNamespace { + return true + } return uri == nil && node.name?.namespaceURI == nil } } @@ -84,7 +86,9 @@ enum RNGNode { guard let colon = raw.firstIndex(of: ":") else { return nil } let prefix = String(raw[.. String { var current: Tree? = node while let candidate = current { - if let library = RNGNode.attribute(candidate, "datatypeLibrary") { return library } + if let library = RNGNode.attribute(candidate, "datatypeLibrary") { + return library + } current = candidate.parent } return "" diff --git a/Sources/Schema/RelaxNGRestrictionChecker.swift b/Sources/Schema/RelaxNGRestrictionChecker.swift index f0be616..9d76a78 100644 --- a/Sources/Schema/RelaxNGRestrictionChecker.swift +++ b/Sources/Schema/RelaxNGRestrictionChecker.swift @@ -23,8 +23,12 @@ final class RelaxNGRestrictionChecker { case let .choice(lhs, rhs): let left = normalized(lhs) let right = normalized(rhs) - if case .notAllowed = left { return right } - if case .notAllowed = right { return left } + if case .notAllowed = left { + return right + } + if case .notAllowed = right { + return left + } return .choice(left, right) case let .group(lhs, rhs): return combineNormalized(normalized(lhs), normalized(rhs), make: { .group($0, $1) }) @@ -41,15 +45,21 @@ final class RelaxNGRestrictionChecker { switch pattern { case let .oneOrMore(inner): let normalizedInner = normalized(inner) - if case .notAllowed = normalizedInner { return .notAllowed } + if case .notAllowed = normalizedInner { + return .notAllowed + } return .oneOrMore(normalizedInner) case let .list(inner): let normalizedInner = normalized(inner) - if case .notAllowed = normalizedInner { return .notAllowed } + if case .notAllowed = normalizedInner { + return .notAllowed + } return .list(normalizedInner) case let .attribute(nameClass, content): let normalizedContent = normalized(content) - if case .notAllowed = normalizedContent { return .notAllowed } + if case .notAllowed = normalizedContent { + return .notAllowed + } return .attribute(nameClass, normalizedContent) case let .element(nameClass, content): return .element(nameClass, normalized(content)) @@ -59,10 +69,18 @@ final class RelaxNGRestrictionChecker { } private static func combineNormalized(_ lhs: Pattern, _ rhs: Pattern, make: (Pattern, Pattern) -> Pattern) -> Pattern { - if case .notAllowed = lhs { return .notAllowed } - if case .notAllowed = rhs { return .notAllowed } - if case .empty = lhs { return rhs } - if case .empty = rhs { return lhs } + if case .notAllowed = lhs { + return .notAllowed + } + if case .notAllowed = rhs { + return .notAllowed + } + if case .empty = lhs { + return rhs + } + if case .empty = rhs { + return lhs + } return make(lhs, rhs) } @@ -117,7 +135,9 @@ final class RelaxNGRestrictionChecker { private func reaches(_ pattern: Pattern, target: String, visiting: Set) -> Bool { switch pattern { case let .ref(name): - if name == target { return true } + if name == target { + return true + } guard !visiting.contains(name) else { return false } return reaches(resolve(name), target: target, visiting: visiting.union([name])) case let .choice(lhs, rhs), let .group(lhs, rhs), let .interleave(lhs, rhs): diff --git a/Sources/Schema/RelaxNGRestrictionOverlap.swift b/Sources/Schema/RelaxNGRestrictionOverlap.swift index b4ea39a..f7392c6 100644 --- a/Sources/Schema/RelaxNGRestrictionOverlap.swift +++ b/Sources/Schema/RelaxNGRestrictionOverlap.swift @@ -26,9 +26,13 @@ extension RelaxNGRestrictionChecker { /// the operands' classes to be pairwise disjoint (7.3). private func combinedAttributeSets(_ lhs: Pattern, _ rhs: Pattern, crossCheck: Bool, visiting: Set) -> (classes: [NameClass], problem: String?) { let left = attributeSets(lhs, visiting: visiting) - if left.problem != nil { return left } + if left.problem != nil { + return left + } let right = attributeSets(rhs, visiting: visiting) - if right.problem != nil { return right } + if right.problem != nil { + return right + } if crossCheck { for first in left.classes { for second in right.classes where nameClassesOverlap(first, second) { diff --git a/Sources/Schema/RelaxNGRestrictionPaths.swift b/Sources/Schema/RelaxNGRestrictionPaths.swift index 3daa762..9a8ca3a 100644 --- a/Sources/Schema/RelaxNGRestrictionPaths.swift +++ b/Sources/Schema/RelaxNGRestrictionPaths.swift @@ -22,9 +22,13 @@ extension RelaxNGRestrictionChecker { switch pattern { case let .element(nameClass, content): seenElements += 1 - if seenElements > 10000 { return nil } + if seenElements > 10000 { + return nil + } _ = nameClass - if let problem = contentProblem(content) { return problem } + if let problem = contentProblem(content) { + return problem + } queue.append(content) case let .attribute(_, content): queue.append(content) @@ -48,11 +52,15 @@ extension RelaxNGRestrictionChecker { /// group/interleave, and list (7.1), a computable content type (7.2), /// distinct attributes (7.3), and interleave constraints (7.4). func contentProblem(_ content: Pattern) -> String? { - if let problem = pathProblem(content, flags: RelaxNGPathFlags(), visiting: []) { return problem } + if let problem = pathProblem(content, flags: RelaxNGPathFlags(), visiting: []) { + return problem + } if contentType(content, visiting: []) == nil { return "element content does not have a computable content type (7.2)" } - if let problem = attributeSets(content, visiting: []).problem { return problem } + if let problem = attributeSets(content, visiting: []).problem { + return problem + } return interleaveProblem(content, visiting: []) } @@ -77,14 +85,20 @@ extension RelaxNGRestrictionChecker { private func compositePathProblem(_ pattern: Pattern, flags: RelaxNGPathFlags, visiting: Set) -> String? { switch pattern { case let .list(inner): - if flags.inList { return "list is not allowed inside list (7.1.3)" } + if flags.inList { + return "list is not allowed inside list (7.1.3)" + } var nested = flags nested.inList = true return pathProblem(inner, flags: nested, visiting: visiting) case let .interleave(lhs, rhs), let .group(lhs, rhs): - if flags.inList, isInterleave(pattern) { return "interleave is not allowed inside list (7.1.3)" } + if flags.inList, isInterleave(pattern) { + return "interleave is not allowed inside list (7.1.3)" + } var nested = flags - if flags.underOneOrMore { nested.inOneOrMoreGroupOrInterleave = true } + if flags.underOneOrMore { + nested.inOneOrMoreGroupOrInterleave = true + } return pathProblem(lhs, flags: nested, visiting: visiting) ?? pathProblem(rhs, flags: nested, visiting: visiting) case let .choice(lhs, rhs): return pathProblem(lhs, flags: flags, visiting: visiting) ?? pathProblem(rhs, flags: flags, visiting: visiting) @@ -100,18 +114,24 @@ extension RelaxNGRestrictionChecker { } private func isInterleave(_ pattern: Pattern) -> Bool { - if case .interleave = pattern { return true } + if case .interleave = pattern { + return true + } return false } /// The attribute prohibitions of 7.1.1-7.1.3 and the infinite-name-class /// rule of 7.3, then the attribute's own content under the inner flag. private func attributePathProblem(_ nameClass: NameClass, _ content: Pattern, flags: RelaxNGPathFlags, visiting: Set) -> String? { - if flags.inAttribute { return "attribute is not allowed inside attribute (7.1.1)" } + if flags.inAttribute { + return "attribute is not allowed inside attribute (7.1.1)" + } if flags.inOneOrMoreGroupOrInterleave { return "attribute is not allowed under oneOrMore with group or interleave (7.1.2)" } - if flags.inList { return "attribute is not allowed inside list (7.1.3)" } + if flags.inList { + return "attribute is not allowed inside list (7.1.3)" + } if !flags.underOneOrMore, isInfinite(nameClass) { return "an attribute with an infinite name class must be inside oneOrMore (7.3)" } @@ -123,8 +143,12 @@ extension RelaxNGRestrictionChecker { /// In the simplified grammar a ref stands for an element, so the /// prohibitions on ref under attribute and list translate to the element. private func elementPathProblem(_ flags: RelaxNGPathFlags) -> String? { - if flags.inAttribute { return "element is not allowed inside attribute (7.1.1)" } - if flags.inList { return "element is not allowed inside list (7.1.3)" } + if flags.inAttribute { + return "element is not allowed inside attribute (7.1.1)" + } + if flags.inList { + return "element is not allowed inside list (7.1.3)" + } return nil // Otherwise a new context, walked separately. } diff --git a/Sources/Schema/RelaxNGRestrictions.swift b/Sources/Schema/RelaxNGRestrictions.swift index 11b5fe3..f59930b 100644 --- a/Sources/Schema/RelaxNGRestrictions.swift +++ b/Sources/Schema/RelaxNGRestrictions.swift @@ -16,7 +16,9 @@ enum RelaxNGRestrictions { } let checker = RelaxNGRestrictionChecker(defines: defines.mapValues(RelaxNGRestrictionChecker.normalized)) let normalizedStart = RelaxNGRestrictionChecker.normalized(start) - if let problem = checker.startProblem(normalizedStart) { return problem } + if let problem = checker.startProblem(normalizedStart) { + return problem + } return checker.elementProblems(normalizedStart) } } diff --git a/Sources/Schema/RelaxNGSchema.swift b/Sources/Schema/RelaxNGSchema.swift index ed83567..573ae7a 100644 --- a/Sources/Schema/RelaxNGSchema.swift +++ b/Sources/Schema/RelaxNGSchema.swift @@ -46,7 +46,9 @@ public extension PureXML.Schema { /// is invalid, falls back to the tree ``errors(in:)`` pass so diagnostics /// stay located rather than a bare false. public func errors(streaming xml: String, limits: PureXML.Parsing.Limits = .default) throws -> [PureXML.Validation.ValidationError] { - if try validate(streaming: xml, limits: limits) { return [] } + if try validate(streaming: xml, limits: limits) { + return [] + } return try errors(in: xml) } diff --git a/Sources/Schema/SchemaAllGroupPlacement.swift b/Sources/Schema/SchemaAllGroupPlacement.swift index 2b60c86..df11eb6 100644 --- a/Sources/Schema/SchemaAllGroupPlacement.swift +++ b/Sources/Schema/SchemaAllGroupPlacement.swift @@ -16,7 +16,9 @@ extension PureXML.Schema.XSDParser { var redefined: Set = [] for container in containers where AllNode.localName(container) == "redefine" { for group in AllNode.children(container, named: "group") { - if let name = AllNode.attribute(group, "name") { redefined.insert(name) } + if let name = AllNode.attribute(group, "name") { + redefined.insert(name) + } } } var allGroups: Set = [] diff --git a/Sources/Schema/SchemaAttributeRestriction.swift b/Sources/Schema/SchemaAttributeRestriction.swift index 9b55cdd..4dbac9f 100644 --- a/Sources/Schema/SchemaAttributeRestriction.swift +++ b/Sources/Schema/SchemaAttributeRestriction.swift @@ -86,8 +86,12 @@ extension PureXML.Schema.XSDParser { // attribute wildcard must admit it, else the restriction adds an // attribute the base never permitted (ctO004: a no-namespace attribute // over a `##other` wildcard). - if isProhibited(derived.name, under: restrictionNode, context) { return nil } - if baseWildcard?.admits(derived.name) == true { return nil } + if isProhibited(derived.name, under: restrictionNode, context) { + return nil + } + if baseWildcard?.admits(derived.name) == true { + return nil + } return "attribute '\(derived.name.localName)' is not in the base type and is not admitted by its attribute wildcard, so a restriction may not add it" } if base.required, !derived.required { diff --git a/Sources/Schema/SchemaAttributeUniqueness.swift b/Sources/Schema/SchemaAttributeUniqueness.swift index 286b5ad..cf5a99f 100644 --- a/Sources/Schema/SchemaAttributeUniqueness.swift +++ b/Sources/Schema/SchemaAttributeUniqueness.swift @@ -199,7 +199,9 @@ extension PureXML.Schema.XSDParser { var idCount = 0 for declaration in declarations where !declaration.isProhibited && seenIDs.insert(declaration.id).inserted { counts[declaration.name, default: 0] += 1 - if declaration.isID { idCount += 1 } + if declaration.isID { + idCount += 1 + } } var errors: [String] = [] for name in counts.keys.sorted(by: { ($0.localName, $0.namespaceURI ?? "") < ($1.localName, $1.namespaceURI ?? "") }) where (counts[name] ?? 0) > 1 { diff --git a/Sources/Schema/SchemaContentDerivation.swift b/Sources/Schema/SchemaContentDerivation.swift index b237908..ad7e7d8 100644 --- a/Sources/Schema/SchemaContentDerivation.swift +++ b/Sources/Schema/SchemaContentDerivation.swift @@ -242,15 +242,23 @@ extension PureXML.Schema.XSDParser { _ types: [String: PureXML.Schema.ElementType], ) -> Bool { if let inline = ContentDerivNode.firstChild(element, named: "complexType") { - if ContentDerivNode.firstChild(inline, named: "simpleContent") != nil { return false } - if ["true", "1"].contains(ContentDerivNode.attribute(inline, "mixed")) { return false } - if ContentDerivNode.firstChild(inline, named: "complexContent") != nil { return false } + if ContentDerivNode.firstChild(inline, named: "simpleContent") != nil { + return false + } + if ["true", "1"].contains(ContentDerivNode.attribute(inline, "mixed")) { + return false + } + if ContentDerivNode.firstChild(inline, named: "complexContent") != nil { + return false + } return ["all", "sequence", "choice", "group"].contains { ContentDerivNode.firstChild(inline, named: $0) != nil } } guard let typeName = ContentDerivNode.attribute(element, "type") else { return false } let namespace = ContentDerivNode.referenceNamespace(typeName, bindings) let local = ContentDerivNode.stripPrefix(typeName) - if namespace == xsdNamespace { return false } + if namespace == xsdNamespace { + return false + } guard namespace == context.targetNamespace, case let .complex(complex)? = types[local] else { return false } switch complex.content { case .elementOnly, .empty: return true @@ -300,8 +308,12 @@ extension PureXML.Schema.XSDParser { /// value matters only when the extension adds element content; otherwise the /// base content is inherited unchanged and there is no mixedness conflict. private static func declaredMixed(_ complexContent: XSDTree, addsElementContent: Bool) -> Bool? { - if let own = ContentDerivNode.attribute(complexContent, "mixed") { return own == "true" || own == "1" } - if let parent = complexContent.parent, let outer = ContentDerivNode.attribute(parent, "mixed") { return outer == "true" || outer == "1" } + if let own = ContentDerivNode.attribute(complexContent, "mixed") { + return own == "true" || own == "1" + } + if let parent = complexContent.parent, let outer = ContentDerivNode.attribute(parent, "mixed") { + return outer == "true" || outer == "1" + } return addsElementContent ? false : nil } } diff --git a/Sources/Schema/SchemaDerivationControl.swift b/Sources/Schema/SchemaDerivationControl.swift index 9e646ff..dd31880 100644 --- a/Sources/Schema/SchemaDerivationControl.swift +++ b/Sources/Schema/SchemaDerivationControl.swift @@ -41,10 +41,16 @@ extension PureXML.Schema.XSDParser { /// control). private static func derivationListError(_ raw: String, allowed: Set) -> String? { let tokens = raw.split(whereSeparator: \.isWhitespace).map(String.init) - if tokens == ["#all"] { return nil } + if tokens == ["#all"] { + return nil + } for token in tokens { - if token == "#all" { return "'#all' may not be combined with other tokens" } - if !allowed.contains(token) { return "'\(token)' is not one of \(allowed.sorted().joined(separator: ", "))" } + if token == "#all" { + return "'#all' may not be combined with other tokens" + } + if !allowed.contains(token) { + return "'\(token)' is not one of \(allowed.sorted().joined(separator: ", "))" + } } return nil } diff --git a/Sources/Schema/SchemaDocument.swift b/Sources/Schema/SchemaDocument.swift index f75fa89..0b002e4 100644 --- a/Sources/Schema/SchemaDocument.swift +++ b/Sources/Schema/SchemaDocument.swift @@ -34,7 +34,9 @@ public extension PureXML.Schema { /// Every compile-time finding when the schema is inconsistent. public var inconsistentFindings: [PureXML.Validation.ValidationError]? { - if case let .inconsistent(findings) = self { return findings } + if case let .inconsistent(findings) = self { + return findings + } return nil } } @@ -251,26 +253,39 @@ public extension PureXML.Schema { driver.consume(event) } var errors = driver.finish() - errors += validator.idErrors() + // Document-scoped ID/IDREF and identity-constraint checking run once the + // whole document has streamed through; both go through the same named + // ``PureXML/Validation`` rules the tree path uses (``idConsistency``, + // `XSD.identityConstraintRules`), not a bare imperative call, so the + // streaming path stays inside the framework like every other validator. + errors += PureXML.Validation.Validator.blank + .validating(PureXML.Schema.ComplexValidator.idConsistency) + .errors(for: validator, in: ()) if !constraints.isEmpty, let identityNode { errors += identityErrors(in: identityNode) } return errors } - /// Identity-constraint findings for an already-parsed instance tree. + /// Identity-constraint findings for an already-parsed instance tree, run + /// through the same decomposed ``PureXML/Validation/XSD`` identity rules + /// the tree path uses. private func identityErrors(in node: PureXML.Model.Node) -> [PureXML.Validation.ValidationError] { guard case let .document(children) = node, let root = children.compactMap(\.element).first else { return [] } - return PureXML.Schema.IdentityValidator( - constraints: constraints, - fieldTypes: identityFieldTypes, - fieldConstraints: identityFieldConstraints, - nonSimpleFields: identityNonSimpleFields, + let context = PureXML.Validation.XSDContext( types: types, + constraints: constraints, + rootDeclaration: nil, + identityFieldTypes: identityFieldTypes, + identityFieldConstraints: identityFieldConstraints, + identityNonSimpleFields: identityNonSimpleFields, ) - .validate(PureXML.Model.TreeNode(.element(root)), at: [.element(root.name.description)]) + return PureXML.Validation.Validator.defaults( + reference: PureXML.Validation.XSD.identityConstraintRules.map { PureXML.Validation.AnyValidation($0) }, + ) + .errors(for: .element(root), in: context) } /// Validates an already-parsed node tree, so a caller can validate the same diff --git a/Sources/Schema/SchemaFacetApplicability.swift b/Sources/Schema/SchemaFacetApplicability.swift index e5319af..54c1511 100644 --- a/Sources/Schema/SchemaFacetApplicability.swift +++ b/Sources/Schema/SchemaFacetApplicability.swift @@ -169,7 +169,9 @@ extension PureXML.Schema.XSDParser { case let .simple(simple): return simple case let .complex(complex): - if case let .simpleContent(simple) = complex.content { return simple } + if case let .simpleContent(simple) = complex.content { + return simple + } return nil case .typeReference: return nil @@ -178,8 +180,12 @@ extension PureXML.Schema.XSDParser { private static func forEachValueConstrained(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } - if local == "element" || local == "attribute" { visit(node) } + if local == "appinfo" || local == "documentation" { + return + } + if local == "element" || local == "attribute" { + visit(node) + } for child in PureXML.Schema.XSDNode.elementChildren(node) { forEachValueConstrained(child, visit) } @@ -226,8 +232,12 @@ extension PureXML.Schema.XSDParser { private static func forEachSimpleType(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } - if local == "simpleType" { visit(node) } + if local == "appinfo" || local == "documentation" { + return + } + if local == "simpleType" { + visit(node) + } for child in PureXML.Schema.XSDNode.elementChildren(node) { forEachSimpleType(child, visit) } @@ -277,7 +287,9 @@ extension PureXML.Schema.XSDParser { private static func forEachRestrictionInSimpleType(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace, local == "simpleType" { for child in PureXML.Schema.XSDNode.elementChildren(node) { if child.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(child) == "restriction" { @@ -330,7 +342,9 @@ extension PureXML.Schema.XSDParser { ) -> Bool { let (prefix, local) = splitQName(base) let uri = prefix.map { bindings[$0] } ?? bindings[""] - if uri == xsdNamespace { return local == "anySimpleType" } + if uri == xsdNamespace { + return local == "anySimpleType" + } guard uri == target, !visited.contains(local), let complexType = globalComplexType(named: local, schema), let simpleContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "simpleContent"), @@ -349,7 +363,9 @@ extension PureXML.Schema.XSDParser { private static func forEachSimpleContentRestriction(_ node: XSDTree, _ visit: (XSDTree) -> Void) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if local == "simpleContent", let restriction = PureXML.Schema.XSDNode.firstChild(node, named: "restriction") { visit(restriction) } diff --git a/Sources/Schema/SchemaIDAttribute.swift b/Sources/Schema/SchemaIDAttribute.swift index c4322c6..e1a5bbe 100644 --- a/Sources/Schema/SchemaIDAttribute.swift +++ b/Sources/Schema/SchemaIDAttribute.swift @@ -23,7 +23,9 @@ extension PureXML.Schema.XSDParser { // `appinfo` and `documentation` hold arbitrary foreign content whose own // `id` attributes are not xs:ID; do not descend into them. let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if let id = unprefixedID(node) { if !PureXML.Schema.Lexical.isNCName(id) { findings.append(PureXML.Schema.SchemaLocatedFinding( diff --git a/Sources/Schema/SchemaIDValueConstraint.swift b/Sources/Schema/SchemaIDValueConstraint.swift index c2eb9e7..53fae18 100644 --- a/Sources/Schema/SchemaIDValueConstraint.swift +++ b/Sources/Schema/SchemaIDValueConstraint.swift @@ -21,7 +21,9 @@ extension PureXML.Schema.XSDParser { into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = IDVNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if let error = idValueConstraintError(node, context) { findings.append(PureXML.Schema.SchemaLocatedFinding(reason: error, node: node)) } diff --git a/Sources/Schema/SchemaIdentityXPath.swift b/Sources/Schema/SchemaIdentityXPath.swift index ab751e8..c3c1f0a 100644 --- a/Sources/Schema/SchemaIdentityXPath.swift +++ b/Sources/Schema/SchemaIdentityXPath.swift @@ -42,7 +42,9 @@ extension PureXML.Schema.XSDParser { for case let .name(name) in tokens { guard let colon = name.firstIndex(of: ":") else { continue } let prefix = String(name[name.startIndex ..< colon]) - if !prefix.isEmpty { prefixes.append(prefix) } + if !prefix.isEmpty { + prefixes.append(prefix) + } } return prefixes } diff --git a/Sources/Schema/SchemaNameUniqueness.swift b/Sources/Schema/SchemaNameUniqueness.swift index 78a79fa..6ff5f9d 100644 --- a/Sources/Schema/SchemaNameUniqueness.swift +++ b/Sources/Schema/SchemaNameUniqueness.swift @@ -54,7 +54,9 @@ extension PureXML.Schema.XSDParser { else { return false } - if prevLocation == location { return true } + if prevLocation == location { + return true + } // A circular import reloads the root schema under a schemaLocation while // the compile root is appended last; treat that as one document, not a clash. let rootIndex = containers.count - 1 @@ -134,7 +136,9 @@ extension PureXML.Schema.XSDParser { /// (`import`/`include`/`redefine`), which the default compile does not load, /// so a `refer` into them is never flagged. private static func keyrefReferFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] { - if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) { return [] } + if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) { + return [] + } var keyArities: [String: Int] = [:] var keyrefs: [KeyrefInfo] = [] let sources = context.compositionLoaded ? containers : [schema] @@ -166,7 +170,9 @@ extension PureXML.Schema.XSDParser { keyArities: inout [String: Int], keyrefs: inout [KeyrefInfo], ) { - if let local = PureXML.Schema.XSDNode.localName(node), local == "appinfo" || local == "documentation" { return } + if let local = PureXML.Schema.XSDNode.localName(node), local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(node) { if local == "key" || local == "unique", let name = PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace() { let arity = PureXML.Schema.XSDNode.elementChildren(node) @@ -203,7 +209,9 @@ extension PureXML.Schema.XSDParser { var findings: [PureXML.Schema.SchemaLocatedFinding] = [] func walk(_ node: XSDTree, namespaceURI: String?) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if let name = identityConstraintName(node) { let key = KeyrefNameKey(targetNamespace: namespaceURI, name: name) if !seen.insert(key).inserted { @@ -238,8 +246,12 @@ extension PureXML.Schema.XSDParser { } private static func isStructurallyEqual(_ lhs: XSDTree, _ rhs: XSDTree) -> Bool { - if lhs.name != rhs.name { return false } - if lhs.attributes.count != rhs.attributes.count { return false } + if lhs.name != rhs.name { + return false + } + if lhs.attributes.count != rhs.attributes.count { + return false + } for attr in lhs.attributes { guard let rhsVal = rhs.attributes.first(where: { $0.name == attr.name })?.value, rhsVal == attr.value @@ -247,7 +259,9 @@ extension PureXML.Schema.XSDParser { } let lhsChildren = PureXML.Schema.XSDNode.elementChildren(lhs) let rhsChildren = PureXML.Schema.XSDNode.elementChildren(rhs) - if lhsChildren.count != rhsChildren.count { return false } + if lhsChildren.count != rhsChildren.count { + return false + } guard !zip(lhsChildren, rhsChildren).contains(where: { !isStructurallyEqual($0, $1) }) else { return false } return true } diff --git a/Sources/Schema/SchemaNestedDefinition.swift b/Sources/Schema/SchemaNestedDefinition.swift index 5ecabc7..1f64d6b 100644 --- a/Sources/Schema/SchemaNestedDefinition.swift +++ b/Sources/Schema/SchemaNestedDefinition.swift @@ -21,7 +21,9 @@ extension PureXML.Schema.XSDParser { let kind = NestedNode.localName(node) // Foreign content inside an annotation is not the schema's own and is not // structurally checked. - if kind == "appinfo" || kind == "documentation" { return } + if kind == "appinfo" || kind == "documentation" { + return + } if isNestedNamedDefinition(node, kind, parentIsTopLevel: parentIsTopLevel) { findings.append(PureXML.Schema.SchemaLocatedFinding( reason: "a nested '\(kind ?? "")' definition may not have a 'name'; only a top-level definition is named", diff --git a/Sources/Schema/SchemaNotationValidity.swift b/Sources/Schema/SchemaNotationValidity.swift index 8e718a2..2aff745 100644 --- a/Sources/Schema/SchemaNotationValidity.swift +++ b/Sources/Schema/SchemaNotationValidity.swift @@ -65,7 +65,9 @@ extension PureXML.Schema.XSDParser { into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace, local == "restriction" { validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &findings) diff --git a/Sources/Schema/SchemaRedefineAttributeGroupRestriction.swift b/Sources/Schema/SchemaRedefineAttributeGroupRestriction.swift index a6365c1..1be9c0e 100644 --- a/Sources/Schema/SchemaRedefineAttributeGroupRestriction.swift +++ b/Sources/Schema/SchemaRedefineAttributeGroupRestriction.swift @@ -54,7 +54,9 @@ extension PureXML.Schema.XSDParser { switch RedefineNode.localName(member) { case "attribute": guard let attrName = RedefineNode.attribute(member, "name") else { return nil } - if RedefineNode.attribute(member, "use") != "prohibited" { names.insert(attrName) } + if RedefineNode.attribute(member, "use") != "prohibited" { + names.insert(attrName) + } case "anyAttribute", "attributeGroup": return nil default: @@ -78,7 +80,9 @@ extension PureXML.Schema.XSDParser { // value (a restriction may not relax the original's fixed constraint). var redefined: [String: XSDTree] = [:] for attribute in children where RedefineNode.localName(attribute) == "attribute" { - if let attrName = RedefineNode.attribute(attribute, "name") { redefined[attrName] = attribute } + if let attrName = RedefineNode.attribute(attribute, "name") { + redefined[attrName] = attribute + } } let use: (String) -> String? = { redefined[$0].map { RedefineNode.attribute($0, "use") ?? "optional" } } let hasReference = children.contains { RedefineNode.localName($0) == "attributeGroup" } @@ -187,7 +191,9 @@ extension PureXML.Schema.XSDParser { /// 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 } + if RedefineNode.attribute(attribute, "form") == "qualified" { + return false + } return RedefineNode.attribute(RedefineNode.schemaOwner(attribute), "attributeFormDefault") != "qualified" } diff --git a/Sources/Schema/SchemaRedefineSelfReference.swift b/Sources/Schema/SchemaRedefineSelfReference.swift index ed4490c..3c148c3 100644 --- a/Sources/Schema/SchemaRedefineSelfReference.swift +++ b/Sources/Schema/SchemaRedefineSelfReference.swift @@ -71,7 +71,9 @@ extension PureXML.Schema.XSDParser { var base: [String: XSDTree] = [:] for container in containers where RedefineNode.localName(container) != "redefine" { for component in RedefineNode.children(container, named: kind) { - if let name = RedefineNode.attribute(component, "name") { base[namespacedKey(name, owner: component)] = component } + if let name = RedefineNode.attribute(component, "name") { + base[namespacedKey(name, owner: component)] = component + } } } return base @@ -136,8 +138,12 @@ extension PureXML.Schema.XSDParser { /// "pointless particle" whose normalization in the redefine-restriction check is /// contested. private static func hasPointlessParticle(_ particle: PureXML.Schema.Particle) -> Bool { - if particle.maxOccurs == 0 { return true } - if case let .group(group) = particle.term { return group.particles.contains(where: hasPointlessParticle) } + if particle.maxOccurs == 0 { + return true + } + if case let .group(group) = particle.term { + return group.particles.contains(where: hasPointlessParticle) + } return false } @@ -161,7 +167,9 @@ extension PureXML.Schema.XSDParser { case "element", "complexType", "simpleType", "attribute": continue case kind: - if RedefineNode.attribute(child, "ref") != nil { nodes.append(child) } + if RedefineNode.attribute(child, "ref") != nil { + nodes.append(child) + } default: nodes += boundedSelfReferenceNodes(child, kind) } diff --git a/Sources/Schema/SchemaReferenceForeignPools.swift b/Sources/Schema/SchemaReferenceForeignPools.swift index 34eef66..785e9b2 100644 --- a/Sources/Schema/SchemaReferenceForeignPools.swift +++ b/Sources/Schema/SchemaReferenceForeignPools.swift @@ -22,7 +22,9 @@ extension PureXML.Schema.XSDParser { let container = containers[index] guard PureXML.Schema.XSDNode.localName(container) != "redefine" else { continue } let namespaceURI = namespaceMap[index] ?? mainTargetNamespace - if namespacesMatch(namespaceURI, mainTargetNamespace) { continue } + if namespacesMatch(namespaceURI, mainTargetNamespace) { + continue + } var pools = result[namespaceURI, default: [:]] insertNamedGlobals(from: container, into: &pools) result[namespaceURI] = pools diff --git a/Sources/Schema/SchemaReferenceResolution.swift b/Sources/Schema/SchemaReferenceResolution.swift index fdc3d43..4f51f8b 100644 --- a/Sources/Schema/SchemaReferenceResolution.swift +++ b/Sources/Schema/SchemaReferenceResolution.swift @@ -60,8 +60,12 @@ extension PureXML.Schema.XSDParser { // target namespace, it cannot name a type that lives there, so it is // undeclared (XSTS addB009 / test66059). This mirrors the same branch in // `isUndeclaredReferenceRef`. - if context.chameleonNamespace, context.types.contains(localPart) { return false } - if !(context.targetNamespace == nil || context.targetNamespace == "") { return true } + if context.chameleonNamespace, context.types.contains(localPart) { + return false + } + if !(context.targetNamespace == nil || context.targetNamespace == "") { + return true + } return !context.types.contains(localPart) } return false @@ -92,8 +96,12 @@ extension PureXML.Schema.XSDParser { return true } if uri == nil || uri == "" { - if context.chameleonNamespace, context.pools[poolName]?.contains(localPart) == true { return false } - if !(context.targetNamespace == nil || context.targetNamespace == "") { return true } + if context.chameleonNamespace, context.pools[poolName]?.contains(localPart) == true { + return false + } + if !(context.targetNamespace == nil || context.targetNamespace == "") { + return true + } return context.pools[poolName]?.contains(localPart) != true } return false diff --git a/Sources/Schema/SchemaReferences.swift b/Sources/Schema/SchemaReferences.swift index 60f1435..7eb9196 100644 --- a/Sources/Schema/SchemaReferences.swift +++ b/Sources/Schema/SchemaReferences.swift @@ -179,7 +179,9 @@ extension PureXML.Schema.XSDParser { into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } let bindings = mergedNamespaceBindings(on: node, inherited: inheritedBindings) let nodeContext = ResolutionContext( types: resolutionContext.check.types, @@ -256,7 +258,9 @@ extension PureXML.Schema.XSDParser { func check(_ node: XSDTree) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace { switch local { @@ -329,7 +333,9 @@ extension PureXML.Schema.XSDParser { func check(_ node: XSDTree) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace { for attribute in ["type", "base", "itemType"] { @@ -362,7 +368,9 @@ extension PureXML.Schema.XSDParser { func check(_ node: XSDTree) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } let isSimpleContentRestriction = node.name?.namespaceURI == xsdNamespace && local == "restriction" && node.parent?.name?.namespaceURI == xsdNamespace diff --git a/Sources/Schema/SchemaSimpleTypeFinal.swift b/Sources/Schema/SchemaSimpleTypeFinal.swift index 9be6d8e..1dd3865 100644 --- a/Sources/Schema/SchemaSimpleTypeFinal.swift +++ b/Sources/Schema/SchemaSimpleTypeFinal.swift @@ -108,7 +108,9 @@ extension PureXML.Schema.XSDParser { guard let raw, !raw.isEmpty else { return [] } var tokens: Set = [] for token in raw.split(whereSeparator: \.isWhitespace) { - if token == "#all" { return ["restriction", "list", "union"] } + if token == "#all" { + return ["restriction", "list", "union"] + } tokens.insert(String(token)) } return tokens diff --git a/Sources/Schema/SchemaSimpleTypeValidity.swift b/Sources/Schema/SchemaSimpleTypeValidity.swift index fc0b2c1..d099e8f 100644 --- a/Sources/Schema/SchemaSimpleTypeValidity.swift +++ b/Sources/Schema/SchemaSimpleTypeValidity.swift @@ -9,7 +9,9 @@ extension PureXML.Schema.XSDParser { func check(_ node: XSDTree) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } if node.name?.namespaceURI == xsdNamespace, local == "list" { let item: PureXML.Schema.SimpleType = if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType") { @@ -104,7 +106,9 @@ extension PureXML.Schema.XSDParser { } private static func isListOrUnionContainingList(_ type: PureXML.Schema.SimpleType) -> Bool { - if type.isAnySimpleType { return true } + if type.isAnySimpleType { + return true + } switch type.variety { case .list: return true diff --git a/Sources/Schema/SchemaSimpleTypeVariety.swift b/Sources/Schema/SchemaSimpleTypeVariety.swift index eda6e44..25a8546 100644 --- a/Sources/Schema/SchemaSimpleTypeVariety.swift +++ b/Sources/Schema/SchemaSimpleTypeVariety.swift @@ -13,7 +13,9 @@ extension PureXML.Schema.XSDParser { init(schema: XSDTree, bindings: [String: String], target: String?) { var byName: [String: XSDTree] = [:] for child in PureXML.Schema.XSDNode.elementChildren(schema) where PureXML.Schema.XSDNode.localName(child) == "simpleType" { - if let name = PureXML.Schema.XSDNode.attribute(child, "name"), byName[name] == nil { byName[name] = child } + if let name = PureXML.Schema.XSDNode.attribute(child, "name"), byName[name] == nil { + byName[name] = child + } } self.byName = byName self.bindings = bindings @@ -33,8 +35,12 @@ extension PureXML.Schema.XSDParser { } private static func simpleTypeVariety(_ simpleType: XSDTree, _ lookup: VarietyLookup, _ visited: Set, _ cache: inout [String: String?]) -> String? { - if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil { return "list" } - if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil { return "union" } + if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil { + return "list" + } + if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil { + return "union" + } if let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction") { return restrictionBaseVariety(restriction, lookup, visited, &cache) } @@ -52,7 +58,9 @@ extension PureXML.Schema.XSDParser { if uri == xsdNamespace { return builtinListTypes.contains(local) ? "list" : nil } - if let cached = cache[local] { return cached } + if let cached = cache[local] { + return cached + } guard uri == lookup.target, !visited.contains(local), let declared = lookup.byName[local] else { return nil } let variety = simpleTypeVariety(declared, lookup, visited.union([local]), &cache) cache[local] = variety diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index 2ca13ff..e126698 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -100,7 +100,9 @@ extension PureXML.Schema.XSDParser { let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName) append(componentSpecificErrors(node, local: local, names: names), at: node, into: &findings) } - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } for child in children { collectStructure(child, bindings: currentBindings, into: &findings) } diff --git a/Sources/Schema/SchemaStructureHelpers.swift b/Sources/Schema/SchemaStructureHelpers.swift index 5fbb32f..e20471e 100644 --- a/Sources/Schema/SchemaStructureHelpers.swift +++ b/Sources/Schema/SchemaStructureHelpers.swift @@ -11,7 +11,9 @@ extension PureXML.Schema.XSDParser { let trimmed = qname.trimmingXMLWhitespace() guard PureXML.Schema.Lexical.isQName(trimmed) else { return nil } if let prefix = PureXML.Schema.XSDNode.prefix(trimmed) { - if prefix == "xml" { return nil } + if prefix == "xml" { + return nil + } if bindings[prefix] == nil { return "prefix '\(prefix)' is not bound" } @@ -31,7 +33,9 @@ extension PureXML.Schema.XSDParser { static func isNonNegativeInteger(_ value: String) -> Bool { var digits = Substring(value) - if digits.first == "+" { digits = digits.dropFirst() } + if digits.first == "+" { + digits = digits.dropFirst() + } return !digits.isEmpty && digits.allSatisfy { $0.isASCII && $0.isNumber } } @@ -88,7 +92,9 @@ extension PureXML.Schema.XSDParser { let isAll = PureXML.Schema.XSDNode.elementChildren(group).contains { $0.name?.namespaceURI == xsd && PureXML.Schema.XSDNode.localName($0) == "all" } - if isAll { allGroupNames.insert(name) } + if isAll { + allGroupNames.insert(name) + } } guard !allGroupNames.isEmpty else { return [] } var findings: [PureXML.Schema.SchemaLocatedFinding] = [] @@ -121,7 +127,9 @@ extension PureXML.Schema.XSDParser { static func canonicalMagnitude(_ value: String) -> Substring { var digits = Substring(value) - if digits.first == "+" { digits = digits.dropFirst() } + if digits.first == "+" { + digits = digits.dropFirst() + } let trimmed = digits.drop { $0 == "0" } return trimmed.isEmpty ? "0" : trimmed } @@ -135,7 +143,9 @@ extension PureXML.Schema.XSDParser { } var seenFacet = false for name in names { - if name == "annotation" { continue } + if name == "annotation" { + continue + } if name == "simpleType" { if seenFacet { errors.append("inline simpleType must appear before facets in restriction") @@ -215,7 +225,9 @@ extension PureXML.Schema.XSDParser { } var seenIdentityConstraint = false for name in names { - if name == "annotation" { continue } + if name == "annotation" { + continue + } if name == "simpleType" || name == "complexType" { if seenIdentityConstraint { errors.append("inline type definition must appear before identity constraints in element") @@ -253,7 +265,9 @@ extension PureXML.Schema.XSDParser { } var seenAnyAttribute = false for name in names { - if name == "annotation" { continue } + if name == "annotation" { + continue + } if name == "attribute" || name == "attributeGroup" { if seenAnyAttribute { errors.append("attribute and attributeGroup references must appear before anyAttribute in attributeGroup") diff --git a/Sources/Schema/SchemaSubstitutionType.swift b/Sources/Schema/SchemaSubstitutionType.swift index 4f09cd3..2bf7835 100644 --- a/Sources/Schema/SchemaSubstitutionType.swift +++ b/Sources/Schema/SchemaSubstitutionType.swift @@ -77,7 +77,9 @@ extension PureXML.Schema.XSDParser { let headNamespace = SubTypeNode.referenceNamespace(headReference, bindings) let headKey = GlobalElementKey(namespaceURI: headNamespace, name: head) guard let headType = globalElementType[headKey] else { return [] } - if isInlineListOrUnion(element) { return [] } + if isInlineListOrUnion(element) { + return [] + } let inlineDeriv = inlineTypeDerivation(element) var isDerived: Bool let memberType: String @@ -252,9 +254,15 @@ extension PureXML.Schema.XSDParser { /// Whether a member type can derive from the simple ur-type. Unknown types are /// left undecided so the reference-resolution rule, not this check, owns them. private static func memberTypeHasSimpleContent(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool? { - if name == "anySimpleType" { return true } - if name == "anyType" { return false } - if PureXML.Schema.BuiltinType(rawValue: name) != nil { return true } + if name == "anySimpleType" { + return true + } + if name == "anyType" { + return false + } + if PureXML.Schema.BuiltinType(rawValue: name) != nil { + return true + } var current: PureXML.Schema.ElementType? = types[name] var steps = 0 while let resolved = current, steps <= types.count { @@ -262,7 +270,9 @@ extension PureXML.Schema.XSDParser { case .simple: return true case let .complex(complex): - if case .simpleContent = complex.content { return true } + if case .simpleContent = complex.content { + return true + } return false case let .typeReference(key): current = types[key] diff --git a/Sources/Schema/SchemaUndeclaredNamespace.swift b/Sources/Schema/SchemaUndeclaredNamespace.swift index cb6843c..5b3f775 100644 --- a/Sources/Schema/SchemaUndeclaredNamespace.swift +++ b/Sources/Schema/SchemaUndeclaredNamespace.swift @@ -14,7 +14,9 @@ extension PureXML.Schema.XSDParser { /// `b`, making `b:b` resolvable), so the whole check stands down to stay lenient. static func undeclaredNamespaceReferenceFindings(_ schema: XSDTree, containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] { let sources = containers.isEmpty ? [schema] : containers - if hasLocatedExternal(sources) { return [] } + if hasLocatedExternal(sources) { + return [] + } var declared: Set = [xsdNamespace] declared.formUnion(unloadedReferenceNamespaces) for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" { @@ -46,7 +48,9 @@ extension PureXML.Schema.XSDParser { into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } let bindings = mergedNamespaceBindings(on: node, inherited: inheritedBindings) if node.name?.namespaceURI == xsdNamespace { for qname in foreignReferenceQNames(of: node) { diff --git a/Sources/Schema/SchemaWildcardNamespace.swift b/Sources/Schema/SchemaWildcardNamespace.swift index 9a40967..520515d 100644 --- a/Sources/Schema/SchemaWildcardNamespace.swift +++ b/Sources/Schema/SchemaWildcardNamespace.swift @@ -23,7 +23,9 @@ extension PureXML.Schema.XSDParser { private static func validWildcardNamespace(_ raw: String) -> Bool { let tokens = raw.split(whereSeparator: \.isWhitespace).map(String.init) // `##any`/`##other` are valid only as the whole value, never as a list item. - if tokens == ["##any"] || tokens == ["##other"] { return true } + if tokens == ["##any"] || tokens == ["##other"] { + return true + } return tokens.allSatisfy { token in switch token { case "##targetNamespace", "##local": true diff --git a/Sources/Schema/SchemaXSIAttribute.swift b/Sources/Schema/SchemaXSIAttribute.swift index cdf6d5a..3acbad9 100644 --- a/Sources/Schema/SchemaXSIAttribute.swift +++ b/Sources/Schema/SchemaXSIAttribute.swift @@ -25,7 +25,9 @@ extension PureXML.Schema.XSDParser { into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = PureXML.Schema.XSDNode.localName(node) - if local == "appinfo" || local == "documentation" { return } + if local == "appinfo" || local == "documentation" { + return + } let isSchema = node.name?.namespaceURI == xsdNamespace && local == "schema" let isAttribute = node.name?.namespaceURI == xsdNamespace && local == "attribute" if isAttribute, PureXML.Schema.XSDNode.attribute(node, "ref") == nil, let name = PureXML.Schema.XSDNode.attribute(node, "name") { diff --git a/Sources/Schema/SimpleType.swift b/Sources/Schema/SimpleType.swift index dd01dd0..b3cc7c3 100644 --- a/Sources/Schema/SimpleType.swift +++ b/Sources/Schema/SimpleType.swift @@ -120,17 +120,27 @@ public extension PureXML.Schema { let normalized = Self.process(lexical, whiteSpace: facets.whiteSpace ?? .collapse) let tokens = normalized.isEmpty ? [] : normalized.split(separator: " ").map(String.init) for token in tokens { - if let error = item.validate(token) { return error } + if let error = item.validate(token) { + return error + } + } + if let exact = facets.length, tokens.count != exact { + return "list length \(tokens.count) is not \(exact)" + } + if let minimum = facets.minLength, tokens.count < minimum { + return "list length \(tokens.count) is below \(minimum)" + } + if let maximum = facets.maxLength, tokens.count > maximum { + return "list length \(tokens.count) is above \(maximum)" } - if let exact = facets.length, tokens.count != exact { return "list length \(tokens.count) is not \(exact)" } - if let minimum = facets.minLength, tokens.count < minimum { return "list length \(tokens.count) is below \(minimum)" } - if let maximum = facets.maxLength, tokens.count > maximum { return "list length \(tokens.count) is above \(maximum)" } if facets.minLength == nil, facets.length == nil { if isBuiltinList, tokens.isEmpty { return "list must not be empty" } } - if let error = patternError(normalized) { return error } + if let error = patternError(normalized) { + return error + } if let enumeration = facets.enumeration, !enumeration.contains(normalized) { return "'\(normalized)' is not in the enumeration" } @@ -147,7 +157,9 @@ public extension PureXML.Schema { /// exact pairwise comparison. var comparesRawLexically: Bool { guard case .atomic = variety, base.primitive.comparesLexically else { return false } - if case .preserve = facets.whiteSpace ?? base.whiteSpace { return true } + if case .preserve = facets.whiteSpace ?? base.whiteSpace { + return true + } return false } @@ -213,12 +225,16 @@ public extension PureXML.Schema { // Pattern matches the lexical value; enumeration compares in the // value space of whichever member admits each value, so `01` equals an // enumerated `1` for an integer member, matching the atomic path. - if let error = patternError(lexical) { return error } + if let error = patternError(lexical) { + return error + } if let enumeration = facets.enumeration { let inEnumeration = enumeration.contains { candidate in members.contains { $0.valueMatches(lexical, literal: candidate) } } - if !inEnumeration { return "'\(lexical)' is not in the enumeration" } + if !inEnumeration { + return "'\(lexical)' is not in the enumeration" + } } return nil } @@ -258,8 +274,12 @@ public extension PureXML.Schema { guard lower != nil || upper != nil, let decimal = DecimalValue(value, allowFraction: false) else { return nil } - if let lower, decimal < lower { return "\(value) is below the minimum for \(base.rawValue)" } - if let upper, decimal > upper { return "\(value) is above the maximum for \(base.rawValue)" } + if let lower, decimal < lower { + return "\(value) is below the minimum for \(base.rawValue)" + } + if let upper, decimal > upper { + return "\(value) is above the maximum for \(base.rawValue)" + } return nil } @@ -268,19 +288,31 @@ public extension PureXML.Schema { // items depending on the type; for QName the spec leaves the unit // unspecified (XSD 1.0 Datatypes 4.3.1), so like Xerces and the XSTS // NIST oracle we do not constrain QName values by length. - if case .qName = primitive { return nil } - if case .notation = primitive { return nil } + if case .qName = primitive { + return nil + } + if case .notation = primitive { + return nil + } let length = primitive.measuredLength(value) - if let exact = facets.length, length != exact { return "length \(length) is not \(exact)" } - if let minimum = facets.minLength, length < minimum { return "length \(length) is below \(minimum)" } - if let maximum = facets.maxLength, length > maximum { return "length \(length) is above \(maximum)" } + if let exact = facets.length, length != exact { + return "length \(length) is not \(exact)" + } + if let minimum = facets.minLength, length < minimum { + return "length \(length) is below \(minimum)" + } + if let maximum = facets.maxLength, length > maximum { + return "length \(length) is above \(maximum)" + } return nil } private func digitsError(_ value: String) -> String? { guard facets.totalDigits != nil || facets.fractionDigits != nil, let decimal = DecimalValue(value, allowFraction: true) else { return nil } - if let total = facets.totalDigits, decimal.totalDigits > total { return "more than \(total) total digits" } + if let total = facets.totalDigits, decimal.totalDigits > total { + return "more than \(total) total digits" + } if let fraction = facets.fractionDigits, decimal.fractionDigits.count > fraction { return "more than \(fraction) fraction digits" } @@ -306,14 +338,18 @@ public extension PureXML.Schema { let whiteSpace = facets.whiteSpace ?? base.whiteSpace let matched = enumeration.contains { candidate in let normalized = Self.process(candidate, whiteSpace: whiteSpace) - if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) { return lhs == rhs } + if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) { + return lhs == rhs + } return normalized == value } return matched ? nil : "'\(value)' is not in the enumeration" } private func rangeError(_ value: String, _ primitive: Primitive) -> String? { - if case .duration = primitive { return durationRangeError(value) } + if case .duration = primitive { + return durationRangeError(value) + } guard let actual = primitive.ordered(value) else { return nil } if let bound = facets.minInclusive, let limit = primitive.ordered(bound), actual < limit { return "\(value) is below the inclusive minimum" diff --git a/Sources/Schema/StepSiblingIndex.swift b/Sources/Schema/StepSiblingIndex.swift index f429dac..46be413 100644 --- a/Sources/Schema/StepSiblingIndex.swift +++ b/Sources/Schema/StepSiblingIndex.swift @@ -32,7 +32,9 @@ final class StepSiblingIndex { } // The grammar predicates a step only when the name is ambiguous; a single // same-name child keeps the bare `name`, so drop a lone entry. - if ordinal < 2 { table = [:] } + if ordinal < 2 { + table = [:] + } positions[parentIdentity, default: [:]][name] = table return table[ObjectIdentifier(node)] } diff --git a/Sources/Schema/StreamingXSDValidator.swift b/Sources/Schema/StreamingXSDValidator.swift index 1e03478..13e7920 100644 --- a/Sources/Schema/StreamingXSDValidator.swift +++ b/Sources/Schema/StreamingXSDValidator.swift @@ -38,7 +38,9 @@ public extension PureXML.Validation { case let .startElement(name, attributes): open(name, attributes) case let .characters(text), let .cdata(text): - if !stack.isEmpty { stack[stack.count - 1].text += text } + if !stack.isEmpty { + stack[stack.count - 1].text += text + } case .endElement: close() case .comment, .processingInstruction: diff --git a/Sources/Schema/XPathQueryCache.swift b/Sources/Schema/XPathQueryCache.swift index ad783e9..16298bd 100644 --- a/Sources/Schema/XPathQueryCache.swift +++ b/Sources/Schema/XPathQueryCache.swift @@ -5,7 +5,9 @@ final class XPathQueryCache { private var queries: [String: PureXML.XPath.Query?] = [:] func query(_ xpath: String) -> PureXML.XPath.Query? { - if let cached = queries[xpath] { return cached } + if let cached = queries[xpath] { + return cached + } let compiled = try? PureXML.XPath.Query(xpath) queries[xpath] = compiled return compiled diff --git a/Sources/Schema/XSDAttributeParser.swift b/Sources/Schema/XSDAttributeParser.swift index d1cce9b..82d8951 100644 --- a/Sources/Schema/XSDAttributeParser.swift +++ b/Sources/Schema/XSDAttributeParser.swift @@ -19,7 +19,9 @@ extension PureXML.Schema.XSDParser { for child in PureXML.Schema.XSDNode.elementChildren(node) { switch PureXML.Schema.XSDNode.localName(child) { case "attribute": - if let use = attributeUse(child, context) { uses.append(use) } + if let use = attributeUse(child, context) { + uses.append(use) + } case "attributeGroup": guard let ref = PureXML.Schema.XSDNode.attribute(child, "ref") else { break } let name = PureXML.Schema.XSDNode.stripPrefix(ref) @@ -112,9 +114,15 @@ extension PureXML.Schema.XSDParser { } else { use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace) } - if PureXML.Schema.XSDNode.attribute(node, "use") == "required" { use.required = true } - if PureXML.Schema.XSDNode.attribute(node, "use") == "prohibited" { use.prohibited = true } - if let constraint = valueConstraint(of: node) { use.valueConstraint = constraint } + if PureXML.Schema.XSDNode.attribute(node, "use") == "required" { + use.required = true + } + if PureXML.Schema.XSDNode.attribute(node, "use") == "prohibited" { + use.prohibited = true + } + if let constraint = valueConstraint(of: node) { + use.valueConstraint = constraint + } return use } diff --git a/Sources/Schema/XSDCompletions.swift b/Sources/Schema/XSDCompletions.swift index 6a35d25..b046e72 100644 --- a/Sources/Schema/XSDCompletions.swift +++ b/Sources/Schema/XSDCompletions.swift @@ -52,14 +52,28 @@ public extension PureXML.Schema { AttributeCompletion(name: $0.name.localName, required: $0.required, present: present.contains($0.name.localName)) } let childNames = element.children.compactMap { child -> PureXML.Model.QualifiedName? in - if case let .element(inner) = child { return inner.name } + if case let .element(inner) = child { + return inner.name + } return nil } switch complex.content { case let .elementOnly(particle), let .mixed(particle): let (labels, complete) = ContentRTNMatcher(ContentRTNBuilder.build(particle)).follow(after: childNames) - let names = labels.compactMap { if case let .name(name) = $0 { name.description } else { nil } } - let anyElement = labels.contains { if case .wildcard = $0 { true } else { false } } + let names = labels.compactMap { + if case let .name(name) = $0 { + name.description + } else { + nil + } + } + let anyElement = labels.contains { + if case .wildcard = $0 { + true + } else { + false + } + } return Completions(elements: names, complete: complete, allowsAnyElement: anyElement, attributes: attributes) case .empty, .simpleContent: return Completions(elements: [], complete: true, allowsAnyElement: false, attributes: attributes) @@ -112,7 +126,9 @@ public extension PureXML.Schema { private static func collect(_ term: Term, into result: inout [String: ElementType]) { switch term { case let .element(name, type, _, _, _, _): - if let type { result[name.description] = type } + if let type { + result[name.description] = type + } case let .group(group): for member in group.particles { collect(member.term, into: &result) diff --git a/Sources/Schema/XSDComplexContent.swift b/Sources/Schema/XSDComplexContent.swift index 6aae0d3..e4cbe63 100644 --- a/Sources/Schema/XSDComplexContent.swift +++ b/Sources/Schema/XSDComplexContent.swift @@ -18,7 +18,9 @@ extension PureXML.Schema.XSDParser { if let base = resolvedBase(derivationNode, context) { var attributes: [PureXML.Schema.AttributeUse] = [] for baseAttr in base.attributes { - if isProhibited(baseAttr.name, under: derivationNode, context) { continue } + if isProhibited(baseAttr.name, under: derivationNode, context) { + continue + } if let derived = allowedOwnAttributes.first(where: { $0.name == baseAttr.name }) { attributes.append(derived) } else { @@ -181,7 +183,9 @@ extension PureXML.Schema.XSDParser { /// type is the redefining occurrence of `baseName`, otherwise the ordinary /// declaration. Returns nil when resolving would form a derivation cycle. private static func baseTypeNode(named baseName: String, _ context: PureXML.Schema.XSDContext) -> XSDTree? { - if let redefined = redefinedBaseNode(named: baseName, context) { return redefined } + if let redefined = redefinedBaseNode(named: baseName, context) { + return redefined + } guard !context.visitingTypes.contains(baseName) else { return nil } return context.complexTypeNodes[baseName] } @@ -204,7 +208,11 @@ extension PureXML.Schema.XSDParser { /// The content of an extension: the base content's particle followed by the /// extension's own particle, in a sequence. Mixed if either side is mixed. private static func extendedContent(base: XSDContentType, own: XSDParticle?, mixed: Bool) -> XSDContentType { - let baseMixed = if case .mixed = base { true } else { false } + let baseMixed = if case .mixed = base { + true + } else { + false + } let combined = combine(particle(of: base), own) return content(combined, mixed: mixed || baseMixed) } diff --git a/Sources/Schema/XSDContext.swift b/Sources/Schema/XSDContext.swift index a32e271..8c46546 100644 --- a/Sources/Schema/XSDContext.swift +++ b/Sources/Schema/XSDContext.swift @@ -29,7 +29,9 @@ extension PureXML.Schema { static func schemaOwner(_ node: XSDTree) -> XSDTree { var current: XSDTree? = node while let element = current { - if localName(element) == "schema" { return element } + if localName(element) == "schema" { + return element + } current = element.parent } return node @@ -64,7 +66,9 @@ extension PureXML.Schema { /// default-namespace binding (nil when none is declared). static func referenceNamespace(_ qualified: String, _ bindings: [String: String]) -> String? { if let prefix = prefix(qualified) { - if prefix == "xml" { return "http://www.w3.org/XML/1998/namespace" } + if prefix == "xml" { + return "http://www.w3.org/XML/1998/namespace" + } return bindings[prefix] } return bindings[""] diff --git a/Sources/Schema/XSDDerivation.swift b/Sources/Schema/XSDDerivation.swift index 51ad335..1c9e18a 100644 --- a/Sources/Schema/XSDDerivation.swift +++ b/Sources/Schema/XSDDerivation.swift @@ -156,7 +156,9 @@ extension PureXML.Schema.XSDParser { var current = start var seen: Set = [] while !redefined.contains(current), !urTypeNames.contains(current), let base = graph[current] { - if base == start { return true } + if base == start { + return true + } guard seen.insert(base).inserted else { return false } current = base } @@ -182,7 +184,9 @@ extension PureXML.Schema.XSDParser { var names: Set = [] for container in containers where XSDDerivNode.localName(container) == "redefine" { for definition in XSDDerivNode.children(container, named: kind) { - if let name = XSDDerivNode.attribute(definition, "name") { names.insert(name) } + if let name = XSDDerivNode.attribute(definition, "name") { + names.insert(name) + } } } return names @@ -235,7 +239,9 @@ extension PureXML.Schema.XSDParser { for container in containers { for definition in descendants(container, named: kind) { guard let name = XSDDerivNode.attribute(definition, "name") else { continue } - if nodeOf[name] == nil { nodeOf[name] = definition } + if nodeOf[name] == nil { + nodeOf[name] = definition + } for ref in boundedReferences(definition, kind) where XSDDerivNode.referenceNamespace(ref, bindings) == target { adjacency[name, default: []].append(XSDDerivNode.stripPrefix(ref)) } @@ -259,7 +265,9 @@ extension PureXML.Schema.XSDParser { case "element", "complexType", "simpleType", "attribute": continue case kind: - if let ref = XSDDerivNode.attribute(child, "ref") { references.append(ref) } + if let ref = XSDDerivNode.attribute(child, "ref") { + references.append(ref) + } default: references += boundedReferences(child, kind) } @@ -273,8 +281,12 @@ extension PureXML.Schema.XSDParser { var visited: Set = [] var stack = adjacency[start] ?? [] while let node = stack.popLast() { - if node == start { return true } - if sinks.contains(node) { continue } + if node == start { + return true + } + if sinks.contains(node) { + continue + } guard visited.insert(node).inserted else { continue } stack += adjacency[node] ?? [] } diff --git a/Sources/Schema/XSDDerivationTables.swift b/Sources/Schema/XSDDerivationTables.swift index 5cf8c36..1e5375a 100644 --- a/Sources/Schema/XSDDerivationTables.swift +++ b/Sources/Schema/XSDDerivationTables.swift @@ -51,7 +51,9 @@ extension PureXML.Schema.XSDParser { private static func gatherSimpleType(_ type: XSDTree, in namespace: String?, into tables: inout DerivationTables) { guard let name = XSDDerivNode.attribute(type, "name") else { return } let finalSet = methodSet(XSDDerivNode.attribute(type, "final")) - if !finalSet.isEmpty { tables.typeFinal[name] = finalSet } + if !finalSet.isEmpty { + tables.typeFinal[name] = finalSet + } // A union's named member types: record them so the xsi:type-block check // can follow a derivation that reaches this union through a member // (cos-st-derived-OK 2.2.4). Inline (anonymous) members carry no name to @@ -62,7 +64,9 @@ extension PureXML.Schema.XSDParser { let memberKeys = members.split(whereSeparator: \.isWhitespace).map { resolvedKey(of: String($0), on: union, default: namespace) } - if !memberKeys.isEmpty { tables.nsUnionMembers[derivationKey(name, in: namespace)] = memberKeys } + if !memberKeys.isEmpty { + tables.nsUnionMembers[derivationKey(name, in: namespace)] = memberKeys + } } guard let restriction = XSDDerivNode.firstChild(type, named: "restriction"), let base = XSDDerivNode.attribute(restriction, "base") else { return } @@ -86,9 +90,15 @@ extension PureXML.Schema.XSDParser { tables.nsTypeBlock[key] = block } let finalSet = finalMethods(of: type) - if !finalSet.isEmpty { tables.typeFinal[name] = finalSet } - if let derivation = derivationInfo(type) { tables.typeDerivation[name] = derivation } - if let derivation = namespacedDerivationInfo(type, in: namespace) { tables.nsTypeDerivation[key] = derivation } + if !finalSet.isEmpty { + tables.typeFinal[name] = finalSet + } + if let derivation = derivationInfo(type) { + tables.typeDerivation[name] = derivation + } + if let derivation = namespacedDerivationInfo(type, in: namespace) { + tables.nsTypeDerivation[key] = derivation + } } private static func gatherElement(_ element: XSDTree, in namespace: String?, into tables: inout DerivationTables) { @@ -101,7 +111,9 @@ extension PureXML.Schema.XSDParser { // so its namespaced key matches the instance element's qualified name. let elementNamespace = isGlobal ? namespace : localElementNamespace(element, in: namespace) let key = derivationKey(name, in: elementNamespace) - if XSDDerivNode.attribute(element, "abstract") == "true" { tables.abstractElements.insert(name) } + if XSDDerivNode.attribute(element, "abstract") == "true" { + tables.abstractElements.insert(name) + } let block = blockMethods(of: element, admitting: [.extension, .restriction, .substitution]) if !block.isEmpty { tables.elementBlock[name] = block @@ -109,7 +121,9 @@ extension PureXML.Schema.XSDParser { } if isGlobal { let finalSet = finalMethods(of: element) - if !finalSet.isEmpty { tables.elementFinal[name] = finalSet } + if !finalSet.isEmpty { + tables.elementFinal[name] = finalSet + } } if let type = XSDDerivNode.attribute(element, "type") { tables.elementTypeNames[name] = XSDDerivNode.stripPrefix(type) diff --git a/Sources/Schema/XSDFinding.swift b/Sources/Schema/XSDFinding.swift new file mode 100644 index 0000000..651a5d2 --- /dev/null +++ b/Sources/Schema/XSDFinding.swift @@ -0,0 +1,15 @@ +/// A validation finding tagged with the decomposed rule it belongs to. +struct XSDFinding { + let rule: XSDRuleToken + let error: XSDFailure + + init(_ rule: XSDRuleToken, reason: String, at path: XSDPath) { + self.rule = rule + error = XSDFailure(reason: reason, at: path) + } + + init(_ rule: XSDRuleToken, _ error: XSDFailure) { + self.rule = rule + self.error = error + } +} diff --git a/Sources/Schema/XSDFindingsCache.swift b/Sources/Schema/XSDFindingsCache.swift new file mode 100644 index 0000000..8e90d8a --- /dev/null +++ b/Sources/Schema/XSDFindingsCache.swift @@ -0,0 +1,12 @@ +/// Memoizes the one full tagged content-validity walk, and the one full tagged +/// identity-constraint walk, per ``PureXML/Validation/XSDContext`` instance. +/// Each decomposed rule's `check` reads this cache (computing it on first use) +/// and filters by its own token, so registering the whole decomposed rule set +/// together still walks the tree exactly once, not once per rule. A fresh +/// `XSDContext` (built once per top-level `validate` call, see +/// `SchemaDocument.validate(_:)`) gets a fresh cache, so there is no staleness +/// risk across documents. +final class XSDFindingsCache: @unchecked Sendable { + var contentFindings: [XSDFinding]? + var identityFindings: [XSDFinding]? +} diff --git a/Sources/Schema/XSDParserHelpers.swift b/Sources/Schema/XSDParserHelpers.swift index c03aa4a..1c60b3d 100644 --- a/Sources/Schema/XSDParserHelpers.swift +++ b/Sources/Schema/XSDParserHelpers.swift @@ -7,8 +7,12 @@ extension PureXML.Schema.XSDParser { for container in containers { for element in descendants(container, named: "element") { guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue } - if PureXML.Schema.XSDNode.attribute(element, "nillable") == "true" { nillable.insert(name) } - if let constraint = valueConstraint(of: element) { constraints[name] = constraint } + if PureXML.Schema.XSDNode.attribute(element, "nillable") == "true" { + nillable.insert(name) + } + if let constraint = valueConstraint(of: element) { + constraints[name] = constraint + } } } return (nillable, constraints) @@ -22,7 +26,9 @@ extension PureXML.Schema.XSDParser { for element in descendants(container, named: "element") { guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue } let constraints = PureXML.Schema.XSDNode.elementChildren(element).compactMap(constraint) - if !constraints.isEmpty { map[name, default: []] += constraints } + if !constraints.isEmpty { + map[name, default: []] += constraints + } } } return map @@ -121,7 +127,9 @@ extension PureXML.Schema.XSDParser { for element in descendants(host, named: "element") { guard let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") else { continue } for attribute in descendants(complex, named: "attribute") where PureXML.Schema.XSDNode.attribute(attribute, "name") == attributeName { - if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context), let value = use.valueConstraint { return value } + if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context), let value = use.valueConstraint { + return value + } } } guard let target = selectorTargetLocalName(constraint.selector), @@ -129,7 +137,9 @@ extension PureXML.Schema.XSDParser { let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") else { return nil } for attribute in descendants(complex, named: "attribute") where PureXML.Schema.XSDNode.attribute(attribute, "name") == attributeName { - if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context), let value = use.valueConstraint { return value } + if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context), let value = use.valueConstraint { + return value + } } return nil } @@ -144,13 +154,17 @@ extension PureXML.Schema.XSDParser { } guard let complex = PureXML.Schema.XSDNode.firstChild(host, named: "complexType") else { return nil } for child in descendants(complex, named: "element") where PureXML.Schema.XSDNode.attribute(child, "name") == local { - if let value = valueConstraint(of: child) { return value } + if let value = valueConstraint(of: child) { + return value + } } return nil } static func identityFieldKey(constraint: PureXML.Schema.IdentityConstraint, field: String, targetLocal: String? = nil) -> String { - if let targetLocal { return "\(constraint.name)|\(field)|\(targetLocal)" } + if let targetLocal { + return "\(constraint.name)|\(field)|\(targetLocal)" + } return "\(constraint.name)|\(field)" } @@ -174,7 +188,9 @@ extension PureXML.Schema.XSDParser { ) -> PureXML.Schema.SimpleType? { if field.hasPrefix("@") { let attributeName = identityFieldAttributeName(field) - if let type = attributeType(named: attributeName, under: host, context) { return type } + if let type = attributeType(named: attributeName, under: host, context) { + return type + } // The selector's target may be a global element referenced from the // host rather than nested under it; the attribute is then on that // element's own complex type, so field values compare in its value @@ -185,7 +201,9 @@ extension PureXML.Schema.XSDParser { else { return nil } for attribute in descendants(complex, named: "attribute") { guard PureXML.Schema.XSDNode.attribute(attribute, "name") == attributeName else { continue } - if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context) { return use.type } + if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context) { + return use.type + } } return nil } @@ -288,7 +306,9 @@ extension PureXML.Schema.XSDParser { static func descendants(_ node: XSDTree, named name: String) -> [XSDTree] { var result: [XSDTree] = [] for child in PureXML.Schema.XSDNode.elementChildren(node) { - if PureXML.Schema.XSDNode.localName(child) == name { result.append(child) } + if PureXML.Schema.XSDNode.localName(child) == name { + result.append(child) + } result += descendants(child, named: name) } return result @@ -343,7 +363,9 @@ extension PureXML.Schema.XSDParser { static func indexByName(_ nodes: [XSDTree]) -> [String: XSDTree] { var index: [String: XSDTree] = [:] for node in nodes { - if let name = PureXML.Schema.XSDNode.attribute(node, "name") { index[name] = node } + if let name = PureXML.Schema.XSDNode.attribute(node, "name") { + index[name] = node + } } return index } @@ -363,8 +385,12 @@ extension PureXML.Schema.XSDParser { } static func valueConstraint(of node: XSDTree) -> PureXML.Schema.ValueConstraint? { - if let fixed = PureXML.Schema.XSDNode.attribute(node, "fixed") { return .fixed(fixed) } - if let value = PureXML.Schema.XSDNode.attribute(node, "default") { return .default(value) } + if let fixed = PureXML.Schema.XSDNode.attribute(node, "fixed") { + return .fixed(fixed) + } + if let value = PureXML.Schema.XSDNode.attribute(node, "default") { + return .default(value) + } return nil } } diff --git a/Sources/Schema/XSDParserParticles.swift b/Sources/Schema/XSDParserParticles.swift index e35aa65..f3ae2fa 100644 --- a/Sources/Schema/XSDParserParticles.swift +++ b/Sources/Schema/XSDParserParticles.swift @@ -47,7 +47,9 @@ extension PureXML.Schema.XSDParser { } var particles: [Particle] = [] for child in XSDNode.elementChildren(node) { - if let member = particle(child, context) { particles.append(member) } + if let member = particle(child, context) { + particles.append(member) + } } // An `all` group's own `minOccurs="0"` makes the GROUP optional (it occurs // zero or one times), not its members optional: when the group is present diff --git a/Sources/Schema/XSDRuleToken.swift b/Sources/Schema/XSDRuleToken.swift new file mode 100644 index 0000000..20a1c82 --- /dev/null +++ b/Sources/Schema/XSDRuleToken.swift @@ -0,0 +1,49 @@ +/// Identifies which decomposed XSD instance-validation rule a finding belongs +/// to. XSD content validation and identity-constraint validation are each one +/// interdependent recursive walk (the #101 decision: a child's effective type +/// is resolved from its parent's content model and position, then rewritten by +/// `xsi:type` and derivation controls mid-walk, so the constraint categories +/// cannot be split into independently-invoked walks without either duplicating +/// the recursion or changing semantics). Tagging every finding at its origin, +/// instead, lets the walk run once while still exposing each constraint +/// category as its own named, independently removable, independently testable +/// ``PureXML/Validation/Validation`` value: see ``XSDFindingsCache``. +enum XSDRuleToken: String, CaseIterable, Sendable { + // MARK: Content validity + + case nilAttributeRequiresNillable + case nilElementMustBeEmpty + case nilElementNoFixedConstraint + case fixedElementNoChildren + case fixedElementValueMatches + case emptyContentForbidsChildren + case simpleContentForbidsChildren + case simpleContentTypeValid + case textForbidden + case wildcardElementUndeclared + case simpleTypeDisallowedAttribute + case simpleTypeForbidsChildren + case simpleTypeValueValid + case typeReferenceResolves + case contentModelStructure + case attributeTypeValid + case attributeFixedMatches + case attributeRequiredPresent + case attributeSingleID + case attributeDeclaredOrPermitted + case wildcardAttributeUndeclared + case wildcardAttributeTypeValid + case wildcardAttributeFixedMatches + case xsiTypeSubstitutionValid + case xsiTypeOverrideResolves + case idIdrefConsistent + + // MARK: Identity constraints + + case identityConstraintXPathsValid + case identityKeyFieldPresent + case identityValueUnique + case identityKeyrefResolves + case identityFieldCardinality + case identityFieldMustBeSimple +} diff --git a/Sources/Schema/XSDSimpleParser+Facets.swift b/Sources/Schema/XSDSimpleParser+Facets.swift index 97314bb..9312f5f 100644 --- a/Sources/Schema/XSDSimpleParser+Facets.swift +++ b/Sources/Schema/XSDSimpleParser+Facets.swift @@ -13,7 +13,9 @@ extension PureXML.Schema.XSDSimpleParser { /// malformed; the canonical form orders two values by length then lexically. static func canonicalNonNegativeInteger(_ lexical: String) -> String? { var digits = Substring(lexical.trimmingXMLWhitespace()) - if digits.first == "+" { digits = digits.dropFirst() } + if digits.first == "+" { + digits = digits.dropFirst() + } guard !digits.isEmpty, digits.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil } let trimmed = digits.drop { $0 == "0" } return trimmed.isEmpty ? "0" : String(trimmed) @@ -30,9 +32,15 @@ extension PureXML.Schema.XSDSimpleParser { let uri = XSDNode.referenceNamespace(typeName, context.namespaceBindings) if uri == PureXML.Schema.XSDParser.xsdNamespace { - if local == "anySimpleType" { return PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true) } - if let builtin = PureXML.Schema.BuiltinType(rawValue: local) { return PureXML.Schema.SimpleType(base: builtin) } - if let item = listBuiltinItem(local) { return .list(item: PureXML.Schema.SimpleType(base: item), isBuiltinList: true) } + if local == "anySimpleType" { + return PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true) + } + if let builtin = PureXML.Schema.BuiltinType(rawValue: local) { + return PureXML.Schema.SimpleType(base: builtin) + } + if let item = listBuiltinItem(local) { + return .list(item: PureXML.Schema.SimpleType(base: item), isBuiltinList: true) + } } return context.simpleTypes[local] ?? PureXML.Schema.SimpleType(base: .string) } @@ -88,7 +96,9 @@ extension PureXML.Schema.XSDSimpleParser { private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout PureXML.Schema.Facets) { switch name { - case "enumeration": if let value { facets.enumeration = (facets.enumeration ?? []) + [value] } + case "enumeration": if let value { + facets.enumeration = (facets.enumeration ?? []) + [value] + } case "minInclusive": facets.minInclusive = value case "maxInclusive": facets.maxInclusive = value case "minExclusive": facets.minExclusive = value diff --git a/Sources/Schema/XSDSimpleTypeOrder.swift b/Sources/Schema/XSDSimpleTypeOrder.swift index d87b584..3ffcbb3 100644 --- a/Sources/Schema/XSDSimpleTypeOrder.swift +++ b/Sources/Schema/XSDSimpleTypeOrder.swift @@ -6,7 +6,9 @@ extension PureXML.Schema.XSDParser { static func simpleTypeNodesByName(_ named: [PureXML.Model.TreeNode]) -> [String: PureXML.Model.TreeNode] { var byName: [String: PureXML.Model.TreeNode] = [:] for node in named { - if let name = XSDNode.attribute(node, "name"), byName[name] == nil { byName[name] = node } + if let name = XSDNode.attribute(node, "name"), byName[name] == nil { + byName[name] = node + } } return byName } @@ -36,7 +38,9 @@ extension PureXML.Schema.XSDParser { order.append(name) continue } - if visited.contains(name) { continue } + if visited.contains(name) { + continue + } visited.insert(name) onPath.insert(name) work.append((name, true)) diff --git a/Sources/Schema/XSDValidations.swift b/Sources/Schema/XSDValidations.swift index 11d8556..d307492 100644 --- a/Sources/Schema/XSDValidations.swift +++ b/Sources/Schema/XSDValidations.swift @@ -17,6 +17,11 @@ public extension PureXML.Validation { public let identityFieldTypes: [String: PureXML.Schema.SimpleType] public let identityFieldConstraints: [String: PureXML.Schema.ValueConstraint] public let identityNonSimpleFields: Set + /// Memoizes the one tagged content-validity walk and the one tagged + /// identity-constraint walk for this context, so the decomposed rule set + /// below (``XSD``) walks the tree once, not once per rule. See + /// ``XSDFindingsCache``. + let findingsCache = XSDFindingsCache() public init( types: [String: PureXML.Schema.ElementType], @@ -53,77 +58,267 @@ public extension PureXML.Validation { } public extension PureXML.Validation { - /// ``XSDContext``, in the OpenAPIKit idiom: each rule is a named, removable - /// ``Validation`` value with a positive description, and the ``validator()`` - /// composes them; failures are located ``ValidationError``s. + /// ``XSDContext``, in the OpenAPIKit idiom: each constraint category is a + /// named, removable ``Validation`` value with a positive description, and + /// ``validator()`` composes them; failures are located ``ValidationError``s. /// - /// Scope note (the #101 decision). XSD content validation is *one* recursive - /// ``Validation`` (``contentValidity``) rather than a separate rule per - /// constraint, because XSD is a recursive type system: a child's type is - /// resolved from its parent's content model and position, then rewritten by - /// `xsi:type` and the derivation controls mid-walk. The constraint categories - /// (attributes, content model, derivation, nillability, fixed values) are - /// interdependent through that resolution, so they cannot be split into - /// independent rules without either changing semantics or duplicating the - /// recursion. This is the sanctioned multi-error form (the rule's `check` - /// returns one located error per violation). Each category is isolation-tested - /// through crafted inputs so it is verifiable on its own, which is the - /// testability the idiom asks for. Identity constraints, which *are* - /// independent, are a separate rule (``identityConstraints``). + /// Decomposition boundary (the #101 decision, extended). XSD content + /// validation and identity-constraint validation are each one recursive walk: + /// a child's type is resolved from its parent's content model and position, + /// then rewritten by `xsi:type` and the derivation controls mid-walk, and + /// identity's `key`/`unique`/`keyref` share one scope stack threaded through + /// one recursive descent. Splitting either walk into independently-INVOKED + /// rules would force either duplicating that recursion (a correctness risk: + /// two walks can drift) or fabricating rules that look independently + /// removable but are not. + /// + /// Instead, every finding is tagged at its origin with the decomposed rule it + /// belongs to (``XSDRuleToken``), the ONE walk is memoized per ``XSDContext`` + /// (``XSDFindingsCache``), and each rule below reads the memoized, tagged + /// result and filters by its own token. The walk still runs exactly once when + /// the full rule set is registered together; each rule is still independently + /// named, described, removable via `withoutValidating`, and isolation-tested + /// (a rule tested alone via `.blank.validating(rule)` still triggers the one + /// full walk and returns just its own slice, so isolation tests are correct + /// without any special-casing). enum XSD { - /// The document element is valid against its declared XSD type. - static var contentValidity: Validation { + /// Every content-validity finding for the document root, tagged by rule, + /// computed once and cached on `context.document.findingsCache`. + private static func contentFindings(for context: ValidationContext) -> [XSDFinding] { + if let cached = context.document.findingsCache.contentFindings { + return cached + } + guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else { + context.document.findingsCache.contentFindings = [] + return [] + } + let schema = context.document + let validator = PureXML.Schema.ComplexValidator( + types: schema.types, + globalAttributes: schema.globalAttributes, + nillableElements: schema.nillableElements, + elementConstraints: schema.elementConstraints, + abstractTypes: schema.abstractTypes, + typeBlock: schema.typeBlock, + elementBlock: schema.elementBlock, + typeDerivation: schema.typeDerivation, + unionMembers: schema.unionMembers, + ) + var findings = validator.validateTagged(root, as: declaration, at: [.element(root.name.description)]) + // Document-scoped ID uniqueness and IDREF resolution, gathered during + // the typed walk above, reported once the tree is seen. + findings += validator.idErrors().map { XSDFinding(.idIdrefConsistent, $0) } + context.document.findingsCache.contentFindings = findings + return findings + } + + /// Every identity-constraint finding for the document root, tagged by + /// rule, computed once and cached on `context.document.findingsCache`. + private static func identityFindings(for context: ValidationContext) -> [XSDFinding] { + if let cached = context.document.findingsCache.identityFindings { + return cached + } + guard case let .element(root) = context.subject else { + context.document.findingsCache.identityFindings = [] + return [] + } + let findings = PureXML.Schema.IdentityValidator( + constraints: context.document.constraints, + fieldTypes: context.document.identityFieldTypes, + fieldConstraints: context.document.identityFieldConstraints, + nonSimpleFields: context.document.identityNonSimpleFields, + types: context.document.types, + ).validateTagged(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)]) + context.document.findingsCache.identityFindings = findings + return findings + } + + /// One decomposed content-validity rule: `description` names the correct + /// state, `token` selects its slice of the one memoized tagged walk. + private static func contentRule(_ token: XSDRuleToken, _ description: String) -> Validation { .init( - description: "The document element is valid against its XSD type", + description: description, check: { context in - guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else { - return [] - } - let schema = context.document - let validator = PureXML.Schema.ComplexValidator( - types: schema.types, - globalAttributes: schema.globalAttributes, - nillableElements: schema.nillableElements, - elementConstraints: schema.elementConstraints, - abstractTypes: schema.abstractTypes, - typeBlock: schema.typeBlock, - elementBlock: schema.elementBlock, - typeDerivation: schema.typeDerivation, - unionMembers: schema.unionMembers, - ) - var errors = validator.validate(root, as: declaration, at: [.element(root.name.description)]) - // Document-scoped ID uniqueness and IDREF resolution, gathered - // during the typed walk above, reported once the tree is seen. - errors += validator.idErrors() - return errors + guard case .element = context.subject else { return [] } + return contentFindings(for: context).filter { $0.rule == token }.map(\.error) }, when: { $0.codingPath.isEmpty }, ) } - /// The document's XSD identity constraints (`unique`, `key`, `keyref`) hold. - static var identityConstraints: Validation { + /// One decomposed identity-constraint rule. See ``contentRule(_:_:)``. + private static func identityRule(_ token: XSDRuleToken, _ description: String) -> Validation { .init( - description: "XSD identity constraints hold", - check: { context -> [PureXML.Validation.ValidationError] in - guard case let .element(root) = context.subject else { return [] } - return PureXML.Schema.IdentityValidator( - constraints: context.document.constraints, - fieldTypes: context.document.identityFieldTypes, - fieldConstraints: context.document.identityFieldConstraints, - nonSimpleFields: context.document.identityNonSimpleFields, - types: context.document.types, - ).validate(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)]) + description: description, + check: { context in + guard case .element = context.subject else { return [] } + return identityFindings(for: context).filter { $0.rule == token }.map(\.error) }, when: { $0.codingPath.isEmpty }, ) } - /// A validator combining the content-model and identity-constraint rules. + // MARK: Content validity + + static var nilAttributeRequiresNillable: Validation { + contentRule(.nilAttributeRequiresNillable, "An element carries xsi:nil only when it is declared nillable") + } + + static var nilElementMustBeEmpty: Validation { + contentRule(.nilElementMustBeEmpty, "A nilled element has no content") + } + + static var nilElementNoFixedConstraint: Validation { + contentRule(.nilElementNoFixedConstraint, "A nilled element carries no fixed value constraint") + } + + static var fixedElementNoChildren: Validation { + contentRule(.fixedElementNoChildren, "A fixed element has no element children") + } + + static var fixedElementValueMatches: Validation { + contentRule(.fixedElementValueMatches, "A fixed element's content equals its fixed value") + } + + static var emptyContentForbidsChildren: Validation { + contentRule(.emptyContentForbidsChildren, "An empty-content element has no element children") + } + + static var simpleContentForbidsChildren: Validation { + contentRule(.simpleContentForbidsChildren, "A simple-content element has no element children") + } + + static var simpleContentTypeValid: Validation { + contentRule(.simpleContentTypeValid, "A simple-content element's text is valid against its type") + } + + static var textForbidden: Validation { + contentRule(.textForbidden, "An element-only or empty-content element carries no text") + } + + static var wildcardElementUndeclared: Validation { + contentRule(.wildcardElementUndeclared, "A strict wildcard-matched element has a global declaration") + } + + static var simpleTypeDisallowedAttribute: Validation { + contentRule(.simpleTypeDisallowedAttribute, "A simple-typed element carries only schema-instance and namespace attributes") + } + + static var simpleTypeForbidsChildren: Validation { + contentRule(.simpleTypeForbidsChildren, "A simple-typed element has no element children") + } + + static var simpleTypeValueValid: Validation { + contentRule(.simpleTypeValueValid, "A simple-typed element's value is valid against its type") + } + + static var typeReferenceResolves: Validation { + contentRule(.typeReferenceResolves, "A type reference resolves to a known, non-circular type") + } + + static var contentModelStructure: Validation { + contentRule(.contentModelStructure, "An element's children conform to its content model") + } + + static var attributeTypeValid: Validation { + contentRule(.attributeTypeValid, "A declared attribute's value is valid against its type") + } + + static var attributeFixedMatches: Validation { + contentRule(.attributeFixedMatches, "A declared attribute's value equals its fixed value") + } + + static var attributeRequiredPresent: Validation { + contentRule(.attributeRequiredPresent, "A required attribute is present") + } + + static var attributeSingleID: Validation { + contentRule(.attributeSingleID, "An element carries at most one attribute of type ID") + } + + static var attributeDeclaredOrPermitted: Validation { + contentRule(.attributeDeclaredOrPermitted, "An attribute is declared or permitted by a wildcard") + } + + static var wildcardAttributeUndeclared: Validation { + contentRule(.wildcardAttributeUndeclared, "A strict wildcard-matched attribute has a global declaration") + } + + static var wildcardAttributeTypeValid: Validation { + contentRule(.wildcardAttributeTypeValid, "A wildcard-matched attribute's value is valid against its type") + } + + static var wildcardAttributeFixedMatches: Validation { + contentRule(.wildcardAttributeFixedMatches, "A wildcard-matched attribute's value equals its fixed value") + } + + static var xsiTypeSubstitutionValid: Validation { + contentRule(.xsiTypeSubstitutionValid, "An xsi:type substitution is validly derived and not blocked") + } + + static var xsiTypeOverrideResolves: Validation { + contentRule(.xsiTypeOverrideResolves, "An xsi:type override names a known, validly derived type") + } + + static var idIdrefConsistent: Validation { + contentRule(.idIdrefConsistent, "Every xs:ID is unique and every xs:IDREF resolves to one") + } + + /// Every decomposed content-validity rule, in application order. + static var contentValidityRules: [Validation] { + [ + nilAttributeRequiresNillable, nilElementMustBeEmpty, nilElementNoFixedConstraint, + fixedElementNoChildren, fixedElementValueMatches, emptyContentForbidsChildren, + simpleContentForbidsChildren, simpleContentTypeValid, textForbidden, + wildcardElementUndeclared, simpleTypeDisallowedAttribute, simpleTypeForbidsChildren, + simpleTypeValueValid, typeReferenceResolves, contentModelStructure, + attributeTypeValid, attributeFixedMatches, attributeRequiredPresent, + attributeSingleID, attributeDeclaredOrPermitted, wildcardAttributeUndeclared, + wildcardAttributeTypeValid, wildcardAttributeFixedMatches, xsiTypeSubstitutionValid, + xsiTypeOverrideResolves, idIdrefConsistent, + ] + } + + // MARK: Identity constraints + + static var identityConstraintXPathsValid: Validation { + identityRule(.identityConstraintXPathsValid, "An identity constraint's selector and field XPaths compile") + } + + static var identityKeyFieldPresent: Validation { + identityRule(.identityKeyFieldPresent, "A key constraint's fields are all present") + } + + static var identityValueUnique: Validation { + identityRule(.identityValueUnique, "A key or unique constraint's values are distinct") + } + + static var identityKeyrefResolves: Validation { + identityRule(.identityKeyrefResolves, "A keyref constraint's value matches a key or unique in scope") + } + + static var identityFieldCardinality: Validation { + identityRule(.identityFieldCardinality, "An identity constraint field selects at most one node") + } + + static var identityFieldMustBeSimple: Validation { + identityRule(.identityFieldMustBeSimple, "An identity constraint field identifies a node with simple content") + } + + /// Every decomposed identity-constraint rule, in application order. + static var identityConstraintRules: [Validation] { + [ + identityConstraintXPathsValid, identityKeyFieldPresent, identityValueUnique, + identityKeyrefResolves, identityFieldCardinality, identityFieldMustBeSimple, + ] + } + + /// A validator combining every decomposed content-model and + /// identity-constraint rule. static func validator() -> Validator { Validator.defaults( - nonReference: [AnyValidation(contentValidity)], - reference: [AnyValidation(identityConstraints)], + nonReference: contentValidityRules.map { AnyValidation($0) }, + reference: identityConstraintRules.map { AnyValidation($0) }, ) } } diff --git a/Sources/Schema/XSDWildcardParser.swift b/Sources/Schema/XSDWildcardParser.swift index f0ec239..3556731 100644 --- a/Sources/Schema/XSDWildcardParser.swift +++ b/Sources/Schema/XSDWildcardParser.swift @@ -83,7 +83,9 @@ extension PureXML.Schema.XSDParser { case "##other": // `##other` = any namespace name other than the target, never absent. // With no target namespace it is simply "any non-absent namespace". - if let target = context.targetNamespace, !target.isEmpty { return .notNamespace(target) } + if let target = context.targetNamespace, !target.isEmpty { + return .notNamespace(target) + } return .notAbsent default: let uris = (value ?? "").split(whereSeparator: \.isWhitespace).map { token -> String in diff --git a/Sources/Validation/AttributeListParser.swift b/Sources/Validation/AttributeListParser.swift index d8aeb9b..6242640 100644 --- a/Sources/Validation/AttributeListParser.swift +++ b/Sources/Validation/AttributeListParser.swift @@ -154,7 +154,9 @@ extension PureXML.Validation { value.append(character) advance() } - if peek() == quote { advance() } + if peek() == quote { + advance() + } return value } @@ -179,7 +181,9 @@ extension PureXML.Validation { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } private mutating func skipSpace() { diff --git a/Sources/Validation/BuiltinValidation.swift b/Sources/Validation/BuiltinValidation.swift index 225e008..3d31ab0 100644 --- a/Sources/Validation/BuiltinValidation.swift +++ b/Sources/Validation/BuiltinValidation.swift @@ -81,16 +81,6 @@ public extension PureXML.Validation { HTML.uniqueIdentifiers } - // MARK: XSD instance - - public static var xsdContentValidity: Validation { - XSD.contentValidity - } - - public static var xsdIdentityConstraints: Validation { - XSD.identityConstraints - } - // MARK: XSD schema consistency public static var xsdFinalRespected: Validation { @@ -113,6 +103,10 @@ public extension PureXML.Validation { PureXML.Schema.ComplexValidator.shallowValidity } + public static var xsdStreamingIdConsistency: Validation { + PureXML.Schema.ComplexValidator.idConsistency + } + /// Stable ids for each shipped builtin rule (matches ``BuiltinValidation`` properties). public static let allRuleIDs: [String] = [ "uniqueAttributes", @@ -133,12 +127,43 @@ public extension PureXML.Validation { "htmlVoidElementsAreEmpty", "htmlRequiredParent", "htmlUniqueIdentifiers", - "xsdContentValidity", - "xsdIdentityConstraints", + "xsdNilAttributeRequiresNillable", + "xsdNilElementMustBeEmpty", + "xsdNilElementNoFixedConstraint", + "xsdFixedElementNoChildren", + "xsdFixedElementValueMatches", + "xsdEmptyContentForbidsChildren", + "xsdSimpleContentForbidsChildren", + "xsdSimpleContentTypeValid", + "xsdTextForbidden", + "xsdWildcardElementUndeclared", + "xsdSimpleTypeDisallowedAttribute", + "xsdSimpleTypeForbidsChildren", + "xsdSimpleTypeValueValid", + "xsdTypeReferenceResolves", + "xsdContentModelStructure", + "xsdAttributeTypeValid", + "xsdAttributeFixedMatches", + "xsdAttributeRequiredPresent", + "xsdAttributeSingleID", + "xsdAttributeDeclaredOrPermitted", + "xsdWildcardAttributeUndeclared", + "xsdWildcardAttributeTypeValid", + "xsdWildcardAttributeFixedMatches", + "xsdXsiTypeSubstitutionValid", + "xsdXsiTypeOverrideResolves", + "xsdIdIdrefConsistent", + "xsdIdentityConstraintXPathsValid", + "xsdIdentityKeyFieldPresent", + "xsdIdentityValueUnique", + "xsdIdentityKeyrefResolves", + "xsdIdentityFieldCardinality", + "xsdIdentityFieldMustBeSimple", "xsdFinalRespected", "xsdRestrictionsAreSubsets", "conformanceMatchesExpected", "xsdStreamingShallowValidity", + "xsdStreamingIdConsistency", ] /// Every builtin rule description, in deterministic order, for configuration-pin tests. @@ -162,12 +187,43 @@ public extension PureXML.Validation { htmlVoidElementsAreEmpty.description, htmlRequiredParent.description, htmlUniqueIdentifiers.description, - xsdContentValidity.description, - xsdIdentityConstraints.description, + xsdNilAttributeRequiresNillable.description, + xsdNilElementMustBeEmpty.description, + xsdNilElementNoFixedConstraint.description, + xsdFixedElementNoChildren.description, + xsdFixedElementValueMatches.description, + xsdEmptyContentForbidsChildren.description, + xsdSimpleContentForbidsChildren.description, + xsdSimpleContentTypeValid.description, + xsdTextForbidden.description, + xsdWildcardElementUndeclared.description, + xsdSimpleTypeDisallowedAttribute.description, + xsdSimpleTypeForbidsChildren.description, + xsdSimpleTypeValueValid.description, + xsdTypeReferenceResolves.description, + xsdContentModelStructure.description, + xsdAttributeTypeValid.description, + xsdAttributeFixedMatches.description, + xsdAttributeRequiredPresent.description, + xsdAttributeSingleID.description, + xsdAttributeDeclaredOrPermitted.description, + xsdWildcardAttributeUndeclared.description, + xsdWildcardAttributeTypeValid.description, + xsdWildcardAttributeFixedMatches.description, + xsdXsiTypeSubstitutionValid.description, + xsdXsiTypeOverrideResolves.description, + xsdIdIdrefConsistent.description, + xsdIdentityConstraintXPathsValid.description, + xsdIdentityKeyFieldPresent.description, + xsdIdentityValueUnique.description, + xsdIdentityKeyrefResolves.description, + xsdIdentityFieldCardinality.description, + xsdIdentityFieldMustBeSimple.description, xsdFinalRespected.description, xsdRestrictionsAreSubsets.description, conformanceMatchesExpected.description, xsdStreamingShallowValidity.description, + xsdStreamingIdConsistency.description, ] } } diff --git a/Sources/Validation/BuiltinValidationXSDInstance.swift b/Sources/Validation/BuiltinValidationXSDInstance.swift new file mode 100644 index 0000000..74d3c6b --- /dev/null +++ b/Sources/Validation/BuiltinValidationXSDInstance.swift @@ -0,0 +1,137 @@ +/// The decomposed XSD instance-validation rules (content validity and identity +/// constraints), split out of ``BuiltinValidation``'s main declaration to keep +/// each file's type body under the length limit. See ``PureXML/Validation/XSD`` +/// for why these are decomposed the way they are. +public extension PureXML.Validation.BuiltinValidation { + // MARK: XSD instance: content validity + + static var xsdNilAttributeRequiresNillable: PureXML.Validation.Validation { + PureXML.Validation.XSD.nilAttributeRequiresNillable + } + + static var xsdNilElementMustBeEmpty: PureXML.Validation.Validation { + PureXML.Validation.XSD.nilElementMustBeEmpty + } + + static var xsdNilElementNoFixedConstraint: PureXML.Validation.Validation { + PureXML.Validation.XSD.nilElementNoFixedConstraint + } + + static var xsdFixedElementNoChildren: PureXML.Validation.Validation { + PureXML.Validation.XSD.fixedElementNoChildren + } + + static var xsdFixedElementValueMatches: PureXML.Validation.Validation { + PureXML.Validation.XSD.fixedElementValueMatches + } + + static var xsdEmptyContentForbidsChildren: PureXML.Validation.Validation { + PureXML.Validation.XSD.emptyContentForbidsChildren + } + + static var xsdSimpleContentForbidsChildren: PureXML.Validation.Validation { + PureXML.Validation.XSD.simpleContentForbidsChildren + } + + static var xsdSimpleContentTypeValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.simpleContentTypeValid + } + + static var xsdTextForbidden: PureXML.Validation.Validation { + PureXML.Validation.XSD.textForbidden + } + + static var xsdWildcardElementUndeclared: PureXML.Validation.Validation { + PureXML.Validation.XSD.wildcardElementUndeclared + } + + static var xsdSimpleTypeDisallowedAttribute: PureXML.Validation.Validation { + PureXML.Validation.XSD.simpleTypeDisallowedAttribute + } + + static var xsdSimpleTypeForbidsChildren: PureXML.Validation.Validation { + PureXML.Validation.XSD.simpleTypeForbidsChildren + } + + static var xsdSimpleTypeValueValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.simpleTypeValueValid + } + + static var xsdTypeReferenceResolves: PureXML.Validation.Validation { + PureXML.Validation.XSD.typeReferenceResolves + } + + static var xsdContentModelStructure: PureXML.Validation.Validation { + PureXML.Validation.XSD.contentModelStructure + } + + static var xsdAttributeTypeValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.attributeTypeValid + } + + static var xsdAttributeFixedMatches: PureXML.Validation.Validation { + PureXML.Validation.XSD.attributeFixedMatches + } + + static var xsdAttributeRequiredPresent: PureXML.Validation.Validation { + PureXML.Validation.XSD.attributeRequiredPresent + } + + static var xsdAttributeSingleID: PureXML.Validation.Validation { + PureXML.Validation.XSD.attributeSingleID + } + + static var xsdAttributeDeclaredOrPermitted: PureXML.Validation.Validation { + PureXML.Validation.XSD.attributeDeclaredOrPermitted + } + + static var xsdWildcardAttributeUndeclared: PureXML.Validation.Validation { + PureXML.Validation.XSD.wildcardAttributeUndeclared + } + + static var xsdWildcardAttributeTypeValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.wildcardAttributeTypeValid + } + + static var xsdWildcardAttributeFixedMatches: PureXML.Validation.Validation { + PureXML.Validation.XSD.wildcardAttributeFixedMatches + } + + static var xsdXsiTypeSubstitutionValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.xsiTypeSubstitutionValid + } + + static var xsdXsiTypeOverrideResolves: PureXML.Validation.Validation { + PureXML.Validation.XSD.xsiTypeOverrideResolves + } + + static var xsdIdIdrefConsistent: PureXML.Validation.Validation { + PureXML.Validation.XSD.idIdrefConsistent + } + + // MARK: XSD instance: identity constraints + + static var xsdIdentityConstraintXPathsValid: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityConstraintXPathsValid + } + + static var xsdIdentityKeyFieldPresent: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityKeyFieldPresent + } + + static var xsdIdentityValueUnique: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityValueUnique + } + + static var xsdIdentityKeyrefResolves: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityKeyrefResolves + } + + static var xsdIdentityFieldCardinality: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityFieldCardinality + } + + static var xsdIdentityFieldMustBeSimple: PureXML.Validation.Validation { + PureXML.Validation.XSD.identityFieldMustBeSimple + } +} diff --git a/Sources/Validation/ContentModelParser.swift b/Sources/Validation/ContentModelParser.swift index 90d6d23..846612a 100644 --- a/Sources/Validation/ContentModelParser.swift +++ b/Sources/Validation/ContentModelParser.swift @@ -17,8 +17,12 @@ extension PureXML.Validation { private mutating func parseModel() -> ContentModel { skipSpace() - if matchKeyword("EMPTY") { return .empty } - if matchKeyword("ANY") { return .any } + if matchKeyword("EMPTY") { + return .empty + } + if matchKeyword("ANY") { + return .any + } guard peek() == "(" else { return .any } // Look inside for the #PCDATA marker to distinguish mixed/PCDATA from @@ -37,14 +41,17 @@ extension PureXML.Validation { var names: [String] = [] while true { skipSpace() - if peek() == ")" { advance() + if peek() == ")" { + advance() break } if peek() == "|" { advance() skipSpace() let name = parseName() - if !name.isEmpty { names.append(name) } + if !name.isEmpty { + names.append(name) + } } else { advance() } @@ -67,7 +74,9 @@ extension PureXML.Validation { items.append(parseParticle()) } skipSpace() - if peek() == ")" { advance() } + if peek() == ")" { + advance() + } let occurrence = consumeOccurrence() return separator == "|" ? .choice(items, occurrence) : .sequence(items, occurrence) } @@ -118,7 +127,9 @@ extension PureXML.Validation { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } private mutating func skipSpace() { diff --git a/Sources/Validation/DTDDefaults.swift b/Sources/Validation/DTDDefaults.swift index e1c9d62..7977ae8 100644 --- a/Sources/Validation/DTDDefaults.swift +++ b/Sources/Validation/DTDDefaults.swift @@ -33,7 +33,9 @@ public extension PureXML.Validation.DTDSchema { let present = result.attributes.contains { $0.name.description == declaration.name || $0.name.localName == declaration.name } - if !present { result.attributes.append(PureXML.Model.Attribute(declaration.name, value)) } + if !present { + result.attributes.append(PureXML.Model.Attribute(declaration.name, value)) + } } return result } diff --git a/Sources/Validation/DTDStandaloneValidity.swift b/Sources/Validation/DTDStandaloneValidity.swift index 4317c05..9878b6d 100644 --- a/Sources/Validation/DTDStandaloneValidity.swift +++ b/Sources/Validation/DTDStandaloneValidity.swift @@ -58,7 +58,9 @@ extension PureXML.Validation.DTD { } // Present attribute whose externally-declared non-CDATA type calls for // normalization the standalone processor would not perform. - if case .cdata = declaration.type { return nil } + if case .cdata = declaration.type { + return nil + } guard supplied.value != normalize(supplied.value, for: declaration.type) else { return nil } return DTDFailure( reason: "standalone document depends on the externally-declared normalization of attribute '\(declaration.name)' on <\(element.name.description)>", diff --git a/Sources/Validation/DTDValidationHelpers.swift b/Sources/Validation/DTDValidationHelpers.swift index ad354af..2d6e63b 100644 --- a/Sources/Validation/DTDValidationHelpers.swift +++ b/Sources/Validation/DTDValidationHelpers.swift @@ -112,7 +112,9 @@ extension PureXML.Validation.DTD { /// arrived through a character reference is data, not separation. CDATA /// values pass through untouched. static func normalize(_ value: String, for type: PureXML.Validation.AttributeType) -> String { - if case .cdata = type { return value } + if case .cdata = type { + return value + } return value.split(separator: " ").joined(separator: " ") } diff --git a/Sources/Validation/Schematron.swift b/Sources/Validation/Schematron.swift index e7ea29c..b9a1aa6 100644 --- a/Sources/Validation/Schematron.swift +++ b/Sources/Validation/Schematron.swift @@ -182,7 +182,9 @@ public extension PureXML.Validation { for id in assertion.diagnostics { guard let parts = diagnostics[id] else { continue } let rendered = render(parts, at: node, variables: variables, functions) - if !rendered.isEmpty { text += " " + rendered } + if !rendered.isEmpty { + text += " " + rendered + } } return text } diff --git a/Sources/Validation/SchematronParser.swift b/Sources/Validation/SchematronParser.swift index 0424e0d..167064d 100644 --- a/Sources/Validation/SchematronParser.swift +++ b/Sources/Validation/SchematronParser.swift @@ -45,7 +45,9 @@ extension PureXML.Validation { let patternNodes = descendants(root, localName: "pattern") var templates: [String: PureXML.Model.TreeNode] = [:] for node in patternNodes where attribute(node, "abstract") == "true" { - if let id = attribute(node, "id") { templates[id] = node } + if let id = attribute(node, "id") { + templates[id] = node + } } var patterns: [SchematronPattern] = [] for node in patternNodes where attribute(node, "abstract") != "true" { @@ -63,7 +65,9 @@ extension PureXML.Validation { private static func parameters(_ node: PureXML.Model.TreeNode) -> [String: String] { var result: [String: String] = [:] for param in children(node, localName: "param") { - if let name = attribute(param, "name"), let value = attribute(param, "value") { result[name] = value } + if let name = attribute(param, "name"), let value = attribute(param, "value") { + result[name] = value + } } return result } diff --git a/Sources/Validation/StreamingDTDValidator.swift b/Sources/Validation/StreamingDTDValidator.swift index 62e0ef0..11a820b 100644 --- a/Sources/Validation/StreamingDTDValidator.swift +++ b/Sources/Validation/StreamingDTDValidator.swift @@ -32,7 +32,9 @@ public extension PureXML.Validation { DTD.tokenizedAttributeTypes, DTD.notationAttributes, ] - if strict { rules.append(DTD.undeclaredElement) } + if strict { + rules.append(DTD.undeclaredElement) + } self.rules = rules } @@ -118,7 +120,9 @@ public extension PureXML.Validation { switch declaration.type { case .id: idCounts[value, default: 0] += 1 - if idCounts[value] == 2 { idDuplicatePath[value] = frame.path } + if idCounts[value] == 2 { + idDuplicatePath[value] = frame.path + } case .idReference: pendingReferences.append(StreamingIDReference(value: value, element: name, path: frame.path)) case .idReferences: diff --git a/Sources/Validation/ValidationCombinators.swift b/Sources/Validation/ValidationCombinators.swift index 31b342b..3d2bafc 100644 --- a/Sources/Validation/ValidationCombinators.swift +++ b/Sources/Validation/ValidationCombinators.swift @@ -25,7 +25,9 @@ func || ( ) -> (VContext) -> [VError] { { context in let left = lhs(context) - if left.isEmpty { return [] } + if left.isEmpty { + return [] + } let right = rhs(context) return right.isEmpty ? [] : left + right } @@ -126,3 +128,21 @@ func lookup( return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) } } } + +/// Resolve an optional named reference from the document: a nil reference is +/// not an error, a named-but-unresolved reference errors, else validate the +/// resolved value. +func unwrapAndLookup( + _ path: KeyPath, + name: KeyPath, + missing: @escaping (String) -> String, + into validations: VRule..., +) -> (VContext) -> [VError] { + { context in + guard let key = context.subject[keyPath: name] else { return [] } + guard let resolved = context.document[keyPath: path][key] else { + return [VError(reason: missing(key), at: context.codingPath)] + } + return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) } + } +} diff --git a/Sources/Validation/Validator.swift b/Sources/Validation/Validator.swift index a635dcc..85b3731 100644 --- a/Sources/Validation/Validator.swift +++ b/Sources/Validation/Validator.swift @@ -150,7 +150,9 @@ public extension PureXML.Validation { public func validate(_ node: PureXML.Model.Node, in document: Document, strict: Bool = true) throws { let split = PureXML.Validation.splitFindings(findings(for: node, in: document)) let toThrow = strict ? split.errors + split.warnings : split.errors - if !toThrow.isEmpty { throw ValidationErrorCollection(values: toThrow) } + if !toThrow.isEmpty { + throw ValidationErrorCollection(values: toThrow) + } } /// Validates `root` and its subtree in document order. The walk is diff --git a/Sources/XInclude/URIReference.swift b/Sources/XInclude/URIReference.swift index 09e6426..9cd8864 100644 --- a/Sources/XInclude/URIReference.swift +++ b/Sources/XInclude/URIReference.swift @@ -63,9 +63,13 @@ extension PureXML.XInclude { var input = Substring(path) var output = "" while !input.isEmpty { - if input.hasPrefix("../") { input = input.dropFirst(3) } else if input.hasPrefix("./") { + if input.hasPrefix("../") { + input = input.dropFirst(3) + } else if input.hasPrefix("./") { input = input.dropFirst(2) - } else if input.hasPrefix("/./") { input = "/" + input.dropFirst(3) } else if input == "/." { + } else if input.hasPrefix("/./") { + input = "/" + input.dropFirst(3) + } else if input == "/." { input = "/" } else if input.hasPrefix("/../") { input = "/" + input.dropFirst(4) @@ -101,11 +105,19 @@ extension PureXML.XInclude { private func recomposed() -> String { var result = "" - if let scheme { result += scheme + ":" } - if let authority { result += "//" + authority } + if let scheme { + result += scheme + ":" + } + if let authority { + result += "//" + authority + } result += path - if let query { result += "?" + query } - if let fragment { result += "#" + fragment } + if let query { + result += "?" + query + } + if let fragment { + result += "#" + fragment + } return result } diff --git a/Sources/XInclude/XIncludeProcessor.swift b/Sources/XInclude/XIncludeProcessor.swift index 61d772c..febb9e4 100644 --- a/Sources/XInclude/XIncludeProcessor.swift +++ b/Sources/XInclude/XIncludeProcessor.swift @@ -103,7 +103,9 @@ private struct XIncludeRun { let resolved = PureXML.XInclude.URIReference.resolve(href, against: elementBase) let isText = attribute(element, "parse") == "text" let xpointer = attribute(element, "xpointer") - if isText, xpointer != nil { throw PureXML.XInclude.XIncludeError.textWithFragment } + if isText, xpointer != nil { + throw PureXML.XInclude.XIncludeError.textWithFragment + } // A resource already on its own inclusion chain is a cycle. guard !context.visited.contains(resolved) else { throw PureXML.XInclude.XIncludeError.cycle(resolved) } guard let content = load(request(element, uri: resolved, isText: isText)) else { @@ -138,7 +140,9 @@ private struct XIncludeRun { private func selectedNodes(from parsed: Node, xpointer: String?) -> [Node] { guard let xpointer else { return [documentElement(of: parsed) ?? parsed] } let selections = (try? PureXML.XPointer.evaluate(xpointer, over: parsed)) ?? [] - if !selections.isEmpty { return selections.map(Self.node) } + if !selections.isEmpty { + return selections.map(Self.node) + } // No node-selecting scheme matched: fall back to the XPointer range model // (range()/range-to()/string-range()) and include each range's content. let ranges = (try? PureXML.XPointer.evaluateRanges(xpointer, over: parsed)) ?? [] @@ -147,7 +151,13 @@ private struct XIncludeRun { private func documentElement(of parsed: Node) -> Node? { guard case let .document(children) = parsed else { return nil } - return children.first { if case .element = $0 { true } else { false } } + return children.first { + if case .element = $0 { + true + } else { + false + } + } } private static func node(_ selection: PureXML.XPath.Selection) -> Node { diff --git a/Sources/XPath/AxisNavigation.swift b/Sources/XPath/AxisNavigation.swift index 58b89d9..f71679b 100644 --- a/Sources/XPath/AxisNavigation.swift +++ b/Sources/XPath/AxisNavigation.swift @@ -100,7 +100,9 @@ extension PureXML.XPath { /// tested first, then its descendants. static func descendantsOrSelf(of tree: PureXML.Model.TreeNode, where keep: (PureXML.Model.TreeNode) -> Bool) -> [Node] { var result: [Node] = [] - if keep(tree) { result.append(.tree(tree)) } + if keep(tree) { + result.append(.tree(tree)) + } appendDescendants(of: tree, where: keep, into: &result) return result } @@ -114,7 +116,9 @@ extension PureXML.XPath { // pushed reversed so they pop in document order. var stack = Array(tree.children.reversed()) while let node = stack.popLast() { - if keep(node) { result.append(.tree(node)) } + if keep(node) { + result.append(.tree(node)) + } if !node.children.isEmpty { stack.append(contentsOf: node.children.reversed()) } @@ -207,7 +211,9 @@ extension PureXML.XPath { rootedAt root: PureXML.Model.TreeNode, cache: DocumentNavigationCache?, ) -> (nodes: [Node], index: [Node: Int]) { - if let cache, let hit = cache.byRoot[ObjectIdentifier(root)] { return hit } + if let cache, let hit = cache.byRoot[ObjectIdentifier(root)] { + return hit + } var nodes: [Node] = [] appendSubtree(root, into: &nodes) var index: [Node: Int] = [:] @@ -264,7 +270,9 @@ extension PureXML.XPath { for attribute in element.attributes { let name = attribute.name if name.prefix == nil, name.localName == "xmlns" { - if bindings[""] == nil { bindings[""] = attribute.value } + if bindings[""] == nil { + bindings[""] = attribute.value + } } else if name.prefix == "xmlns", bindings[name.localName] == nil { bindings[name.localName] = attribute.value } diff --git a/Sources/XPath/Compiler.swift b/Sources/XPath/Compiler.swift index 7e84b9a..d80d0b9 100644 --- a/Sources/XPath/Compiler.swift +++ b/Sources/XPath/Compiler.swift @@ -74,8 +74,12 @@ extension PureXML.XPath { private mutating func relationalOperator() -> BinaryOperator? { skipSpace() - if consume("<=") { return .lessOrEqual } - if consume(">=") { return .greaterOrEqual } + if consume("<=") { + return .lessOrEqual + } + if consume(">=") { + return .greaterOrEqual + } if peek() == "<" { advance() return .lessThan @@ -169,7 +173,9 @@ extension PureXML.XPath { /// character), used to read operator keywords in operator position. func peekKeyword(_ word: String) -> Bool { guard matches(word) else { return false } - if let after = peek(word.count), after.isXMLNameContinuation { return false } + if let after = peek(word.count), after.isXMLNameContinuation { + return false + } return true } diff --git a/Sources/XPath/CompilerPaths.swift b/Sources/XPath/CompilerPaths.swift index 4d46c57..bac3057 100644 --- a/Sources/XPath/CompilerPaths.swift +++ b/Sources/XPath/CompilerPaths.swift @@ -36,8 +36,12 @@ extension PureXML.XPath.Compiler { if character == "$" || character == "(" || character == "\"" || character == "'" { return true } - if character.isNumber { return true } - if character == ".", peek(1)?.isNumber == true { return true } + if character.isNumber { + return true + } + if character == ".", peek(1)?.isNumber == true { + return true + } return functionCallAhead() } @@ -302,7 +306,9 @@ extension PureXML.XPath.Compiler { mutating func parseName() -> String { var name = "" while let character = peek(), character.isXMLNameContinuation { - if character == ":", peek(1) == ":" { break } + if character == ":", peek(1) == ":" { + break + } name.append(character) advance() } diff --git a/Sources/XPath/CoreFunctions.swift b/Sources/XPath/CoreFunctions.swift index 1df583e..a40fb16 100644 --- a/Sources/XPath/CoreFunctions.swift +++ b/Sources/XPath/CoreFunctions.swift @@ -21,11 +21,15 @@ extension PureXML.XPath { "false": { _, _ in .boolean(false) }, "boolean": { arguments, _ in .boolean(arguments.first?.boolean ?? false) }, "number": { arguments, context in - if let first = arguments.first { return .number(first.number) } + if let first = arguments.first { + return .number(first.number) + } return .number(PureXML.XPath.Value.parseNumber(context.node.stringValue)) }, "string": { arguments, context in - if let first = arguments.first { return .string(first.string) } + if let first = arguments.first { + return .string(first.string) + } return .string(context.node.stringValue) }, ]) diff --git a/Sources/XPath/DocumentOrder.swift b/Sources/XPath/DocumentOrder.swift index 2284bbd..2228211 100644 --- a/Sources/XPath/DocumentOrder.swift +++ b/Sources/XPath/DocumentOrder.swift @@ -12,9 +12,15 @@ extension PureXML.XPath.AxisNavigation { var sub = 0 static func < (lhs: OrderKey, rhs: OrderKey) -> Bool { - if lhs.rootRank != rhs.rootRank { return lhs.rootRank < rhs.rootRank } - if lhs.index != rhs.index { return lhs.index < rhs.index } - if lhs.band != rhs.band { return lhs.band < rhs.band } + if lhs.rootRank != rhs.rootRank { + return lhs.rootRank < rhs.rootRank + } + if lhs.index != rhs.index { + return lhs.index < rhs.index + } + if lhs.band != rhs.band { + return lhs.band < rhs.band + } return lhs.sub < rhs.sub } } diff --git a/Sources/XPath/EXSLTFunctions.swift b/Sources/XPath/EXSLTFunctions.swift index 14c9ad3..0743ab0 100644 --- a/Sources/XPath/EXSLTFunctions.swift +++ b/Sources/XPath/EXSLTFunctions.swift @@ -51,7 +51,9 @@ extension PureXML.XPath { private static func extremum(_ value: Value?, isMin: Bool) -> Double { guard let nodes = value?.nodes, !nodes.isEmpty else { return .nan } let numbers = nodes.map { PureXML.XPath.Value.parseNumber($0.stringValue) } - if numbers.contains(where: \.isNaN) { return .nan } + if numbers.contains(where: \.isNaN) { + return .nan + } return isMin ? numbers.min() ?? .nan : numbers.max() ?? .nan } diff --git a/Sources/XPath/Evaluator.swift b/Sources/XPath/Evaluator.swift index a0749fe..016efc8 100644 --- a/Sources/XPath/Evaluator.swift +++ b/Sources/XPath/Evaluator.swift @@ -154,8 +154,12 @@ extension PureXML.XPath { /// QName): the raw key is tried first, so a caller's raw binding is /// unaffected, then the namespace-resolved name. private static func resolveVariable(_ name: String, _ context: EvaluationContext) throws -> Value { - if let value = context.variables[name] { return value } - if let expanded = expandedKey(name, context.namespaces), let value = context.variables[expanded] { return value } + if let value = context.variables[name] { + return value + } + if let expanded = expandedKey(name, context.namespaces), let value = context.variables[expanded] { + return value + } throw QueryError.undefinedVariable(name) } diff --git a/Sources/XPath/EvaluatorOperators.swift b/Sources/XPath/EvaluatorOperators.swift index b115a6e..a0199d5 100644 --- a/Sources/XPath/EvaluatorOperators.swift +++ b/Sources/XPath/EvaluatorOperators.swift @@ -71,9 +71,13 @@ extension PureXML.XPath.Evaluator { } private static func scalarEquality(_ lhs: Value, _ rhs: Value, negate: Bool) -> Bool { - let equal: Bool = if case .boolean = lhs { lhs.boolean == rhs.boolean } else if case .boolean = rhs { + let equal: Bool = if case .boolean = lhs { lhs.boolean == rhs.boolean - } else if case .number = lhs { lhs.number == rhs.number } else if case .number = rhs { + } else if case .boolean = rhs { + lhs.boolean == rhs.boolean + } else if case .number = lhs { + lhs.number == rhs.number + } else if case .number = rhs { lhs.number == rhs.number } else { lhs.string == rhs.string diff --git a/Sources/XPath/EvaluatorSteps.swift b/Sources/XPath/EvaluatorSteps.swift index 0004187..1b71323 100644 --- a/Sources/XPath/EvaluatorSteps.swift +++ b/Sources/XPath/EvaluatorSteps.swift @@ -232,20 +232,26 @@ extension PureXML.XPath.Evaluator { case let .name(name): let kind = axis.principalKind return { node in - if case let .tree(tree) = node { return treeTest(tree) } + if case let .tree(tree) = node { + return treeTest(tree) + } return nameMatches(node, name, kind, namespaces) } case .wildcard: let kind = axis.principalKind return { node in - if case let .tree(tree) = node { return treeTest(tree) } + if case let .tree(tree) = node { + return treeTest(tree) + } return wildcardMatches(node, kind) } case .node: return { _ in true } case .text, .comment, .processingInstruction: return { node in - if case let .tree(tree) = node { return treeTest(tree) } + if case let .tree(tree) = node { + return treeTest(tree) + } return false } } @@ -303,7 +309,9 @@ extension PureXML.XPath.Evaluator { if let uri = namespaces[prefix] { let local = String(name[name.index(after: colon)...]) // The NCName:* form matches every name in the namespace. - if local == "*" { return (qualified.namespaceURI ?? "") == uri } + if local == "*" { + return (qualified.namespaceURI ?? "") == uri + } return qualified.localName == local && (qualified.namespaceURI ?? "") == uri } } else { @@ -323,9 +331,13 @@ extension PureXML.XPath.Evaluator { private static func wildcardMatches(_ node: Node, _ kind: PrincipalKind) -> Bool { switch kind { case .element: return isTreeKind(node, in: [.element]) - case .attribute: if case .attribute = node { return true } + case .attribute: if case .attribute = node { + return true + } return false - case .namespace: if case .namespace = node { return true } + case .namespace: if case .namespace = node { + return true + } return false } } diff --git a/Sources/XPath/NodeFunctions.swift b/Sources/XPath/NodeFunctions.swift index 6f783a9..7842047 100644 --- a/Sources/XPath/NodeFunctions.swift +++ b/Sources/XPath/NodeFunctions.swift @@ -32,7 +32,9 @@ extension PureXML.XPath { } private static func target(_ arguments: [Value], _ context: EvaluationContext) -> Node? { - if arguments.isEmpty { return context.node } + if arguments.isEmpty { + return context.node + } return arguments.first?.nodes?.firstInDocumentOrder() } diff --git a/Sources/XPath/StringFunctions.swift b/Sources/XPath/StringFunctions.swift index f078610..0544328 100644 --- a/Sources/XPath/StringFunctions.swift +++ b/Sources/XPath/StringFunctions.swift @@ -67,7 +67,9 @@ extension PureXML.XPath { while prefix > 0, needle[index] != needle[prefix] { prefix = failure[prefix - 1] } - if needle[index] == needle[prefix] { prefix += 1 } + if needle[index] == needle[prefix] { + prefix += 1 + } failure[index] = prefix } var matched = 0 @@ -75,8 +77,12 @@ extension PureXML.XPath { while matched > 0, haystack[index] != needle[matched] { matched = failure[matched - 1] } - if haystack[index] == needle[matched] { matched += 1 } - if matched == needle.count { return index - needle.count + 1 } + if haystack[index] == needle[matched] { + matched += 1 + } + if matched == needle.count { + return index - needle.count + 1 + } } return nil } @@ -112,7 +118,9 @@ extension PureXML.XPath { var replace: [Character: Character] = [:] var delete: Set = [] for (index, character) in from.enumerated() { - if replace[character] != nil || delete.contains(character) { continue } + if replace[character] != nil || delete.contains(character) { + continue + } if index < replacements.count { replace[character] = replacements[index] } else { @@ -134,7 +142,9 @@ extension PureXML.XPath { /// XPath rounding: half rounds toward positive infinity, with NaN and /// infinities passed through. static func rounded(_ value: Double) -> Double { - if value.isNaN || value.isInfinite { return value } + if value.isNaN || value.isInfinite { + return value + } return (value + 0.5).rounded(.down) } } diff --git a/Sources/XPath/Value.swift b/Sources/XPath/Value.swift index 431a4c4..1e374fa 100644 --- a/Sources/XPath/Value.swift +++ b/Sources/XPath/Value.swift @@ -56,8 +56,12 @@ public extension PureXML.XPath { /// XPath 1.0 forbids exponential notation, so any exponent in the shortest /// representation is expanded to a plain decimal. static func format(_ value: Double) -> String { - if value.isNaN { return "NaN" } - if value.isInfinite { return value < 0 ? "-Infinity" : "Infinity" } + if value.isNaN { + return "NaN" + } + if value.isInfinite { + return value < 0 ? "-Infinity" : "Infinity" + } // An integer-valued double prints with no decimal point. The bound is // 2^63 so the Int64 conversion cannot trap; exact integers run to 2^53 // and the representable ones above it are still whole, and a magnitude @@ -114,9 +118,13 @@ public extension PureXML.XPath { var seenDot = false var digits = 0 for (offset, character) in trimmed.enumerated() { - if character == "-", offset == 0 { continue } + if character == "-", offset == 0 { + continue + } if character == "." { - if seenDot { return .nan } + if seenDot { + return .nan + } seenDot = true continue } diff --git a/Sources/XPointer/Pointer.swift b/Sources/XPointer/Pointer.swift index 94dca86..d772ca3 100644 --- a/Sources/XPointer/Pointer.swift +++ b/Sources/XPointer/Pointer.swift @@ -52,7 +52,9 @@ private struct PointerScanner { while let character = peek() { advance() if let active = quote { - if character == active { quote = nil } + if character == active { + quote = nil + } data.append(character) continue } @@ -63,7 +65,9 @@ private struct PointerScanner { depth += 1 case ")": depth -= 1 - if depth == 0 { return data } + if depth == 0 { + return data + } default: break } @@ -75,7 +79,9 @@ private struct PointerScanner { private func translate(scheme: String, data: String) throws -> PointerPart { switch scheme { case "xpointer", "xpath1": - if let form = PureXML.XPointer.RangeForm.parse(data) { return .range(form) } + if let form = PureXML.XPointer.RangeForm.parse(data) { + return .range(form) + } return .expression(data) case "element": return .expression(Self.elementToXPath(data)) case "xmlns": return try Self.namespaceBinding(data) @@ -133,7 +139,9 @@ private struct PointerScanner { } private mutating func advance() { - if index < chars.count { index += 1 } + if index < chars.count { + index += 1 + } } private mutating func skipSpace() { @@ -174,7 +182,9 @@ public extension PureXML.XPointer { case let .expression(xpath): guard let query = try? PureXML.XPath.Query(xpath) else { continue } let result = query.evaluate(over: node, namespaces: namespaces) - if !result.isEmpty { return result } + if !result.isEmpty { + return result + } case .range: continue } @@ -197,7 +207,9 @@ public extension PureXML.XPointer { namespaces[prefix] = uri case let .range(form): let ranges = form.ranges(over: root, namespaces: namespaces) - if !ranges.isEmpty { return ranges } + if !ranges.isEmpty { + return ranges + } case .expression: continue } diff --git a/Sources/XPointer/PointerRange.swift b/Sources/XPointer/PointerRange.swift index c22fa47..7394cb7 100644 --- a/Sources/XPointer/PointerRange.swift +++ b/Sources/XPointer/PointerRange.swift @@ -68,7 +68,9 @@ public extension PureXML.XPointer { while index < characters.count { let character = characters[index] if let active = quote { - if character == active { quote = nil } + if character == active { + quote = nil + } } else if character == "'" || character == "\"" { quote = character } else if character == "(" { @@ -104,7 +106,9 @@ public extension PureXML.XPointer { var quote: Character? for character in value { if let active = quote { - if character == active { quote = nil } + if character == active { + quote = nil + } current.append(character) } else if character == "'" || character == "\"" { quote = character @@ -180,7 +184,9 @@ extension PureXML.XPointer.RangeForm { while matched > 0, needle[index] != needle[matched] { matched = failure[matched - 1] } - if needle[index] == needle[matched] { matched += 1 } + if needle[index] == needle[matched] { + matched += 1 + } failure[index] = matched } return failure diff --git a/Sources/XSLT/XSLTAVT.swift b/Sources/XSLT/XSLTAVT.swift index 8233691..7dd64e1 100644 --- a/Sources/XSLT/XSLTAVT.swift +++ b/Sources/XSLT/XSLTAVT.swift @@ -19,7 +19,8 @@ extension PureXML.XSLT.XSLTParser { continue } if character == "{" { - if !literal.isEmpty { parts.append(.literal(literal)) + if !literal.isEmpty { + parts.append(.literal(literal)) literal = "" } index += 1 @@ -35,7 +36,9 @@ extension PureXML.XSLT.XSLTParser { literal.append(character) index += 1 } - if !literal.isEmpty { parts.append(.literal(literal)) } + if !literal.isEmpty { + parts.append(.literal(literal)) + } return parts } } diff --git a/Sources/XSLT/XSLTCollation.swift b/Sources/XSLT/XSLTCollation.swift index 0247c8a..0d14097 100644 --- a/Sources/XSLT/XSLTCollation.swift +++ b/Sources/XSLT/XSLTCollation.swift @@ -39,9 +39,13 @@ extension PureXML.XSLT { for (first, second) in zip(left, right) where first != second { let leftRank = ranks[first] ?? beyond + Int(first.unicodeScalars.first?.value ?? 0) let rightRank = ranks[second] ?? beyond + Int(second.unicodeScalars.first?.value ?? 0) - if leftRank != rightRank { return leftRank < rightRank ? -1 : 1 } + if leftRank != rightRank { + return leftRank < rightRank ? -1 : 1 + } + } + if left.count != right.count { + return left.count < right.count ? -1 : 1 } - if left.count != right.count { return left.count < right.count ? -1 : 1 } return left == right ? 0 : (left < right ? -1 : 1) } } diff --git a/Sources/XSLT/XSLTDocumentCache.swift b/Sources/XSLT/XSLTDocumentCache.swift index eb19b94..d11c7d0 100644 --- a/Sources/XSLT/XSLTDocumentCache.swift +++ b/Sources/XSLT/XSLTDocumentCache.swift @@ -62,7 +62,9 @@ extension PureXML.XSLT { let names = PureXML.Validation.AttributeListParser.parse(body) .filter { $0.type == .id } .map(\.name) - if !names.isEmpty { declared[element, default: []].formUnion(names) } + if !names.isEmpty { + declared[element, default: []].formUnion(names) + } } return declared } diff --git a/Sources/XSLT/XSLTElements.swift b/Sources/XSLT/XSLTElements.swift index 1cf229d..953cd5c 100644 --- a/Sources/XSLT/XSLTElements.swift +++ b/Sources/XSLT/XSLTElements.swift @@ -4,8 +4,12 @@ extension PureXML.XSLT.Transformer { static func caseInsensitiveCompare(_ left: String, _ right: String, _ caseOrder: PureXML.XSLT.CaseOrder) -> Int { let leftLower = left.lowercased() let rightLower = right.lowercased() - if leftLower != rightLower { return leftLower < rightLower ? -1 : 1 } - if left == right { return 0 } + if leftLower != rightLower { + return leftLower < rightLower ? -1 : 1 + } + if left == right { + return 0 + } // Equal apart from case: codepoint order puts uppercase first. let upperFirst = left < right let leftFirst = caseOrder == .upperFirst ? upperFirst : !upperFirst @@ -116,7 +120,9 @@ extension PureXML.XSLT.Transformer { current = candidate.parent } return bindings.sorted(by: { $0.key < $1.key }).compactMap { prefix, uri in - if prefix.isEmpty, uri.isEmpty { return nil } + if prefix.isEmpty, uri.isEmpty { + return nil + } let name = prefix.isEmpty ? "xmlns" : "xmlns:" + prefix return PureXML.XSLT.LiteralAttribute(name: PureXML.Model.QualifiedName(name), value: [.literal(uri)]) } diff --git a/Sources/XSLT/XSLTFunctions.swift b/Sources/XSLT/XSLTFunctions.swift index a15cba2..60ccfa8 100644 --- a/Sources/XSLT/XSLTFunctions.swift +++ b/Sources/XSLT/XSLTFunctions.swift @@ -37,7 +37,9 @@ extension PureXML.XSLT { layout.prefix = String(symbols.minusSign) + layout.prefix } } - if value.isInfinite { return layout.prefix + symbols.infinity + layout.suffix } + if value.isInfinite { + return layout.prefix + symbols.infinity + layout.suffix + } let magnitude = Swift.abs(value) * positive.multiplier return layout.prefix + render(magnitude, layout, symbols) + layout.suffix } @@ -57,7 +59,9 @@ extension PureXML.XSLT { for character in subpattern { if character == symbols.percent || character == symbols.perMille { layout.multiplier = character == symbols.percent ? 100 : 1000 - if phase == 1 { phase = 2 } + if phase == 1 { + phase = 2 + } appendAffix(character, phase, &layout) continue } @@ -70,7 +74,9 @@ extension PureXML.XSLT { } continue } - if phase == 1 { phase = 2 } + if phase == 1 { + phase = 2 + } appendAffix(character, phase, &layout) } layout.groupingSize = sinceGrouping ?? 0 @@ -87,12 +93,18 @@ extension PureXML.XSLT { _ sinceGrouping: inout Int?, ) { if character == symbols.groupingSeparator { - if !inFraction { sinceGrouping = 0 } + if !inFraction { + sinceGrouping = 0 + } } else if inFraction { layout.maxFraction += 1 - if character == symbols.zeroDigit { layout.minFraction += 1 } + if character == symbols.zeroDigit { + layout.minFraction += 1 + } } else { - if character == symbols.zeroDigit { layout.minInteger += 1 } + if character == symbols.zeroDigit { + layout.minInteger += 1 + } sinceGrouping = sinceGrouping.map { $0 + 1 } } } @@ -126,7 +138,9 @@ extension PureXML.XSLT { paddedInteger = "0" + paddedInteger } var integerText = digitsToSymbols(paddedInteger, symbols) - if layout.groupingSize > 0 { integerText = group(integerText, symbols.groupingSeparator, layout.groupingSize) } + if layout.groupingSize > 0 { + integerText = group(integerText, symbols.groupingSeparator, layout.groupingSize) + } var fractionDigits = allFractionDigits while fractionDigits.count > layout.minFraction, fractionDigits.hasSuffix("0") { @@ -174,7 +188,9 @@ extension PureXML.XSLT { private static func group(_ digits: String, _ separator: Character, _ size: Int) -> String { var result = "" for (offset, character) in digits.reversed().enumerated() { - if offset > 0, offset.isMultiple(of: size) { result.append(separator) } + if offset > 0, offset.isMultiple(of: size) { + result.append(separator) + } result.append(character) } return String(result.reversed()) @@ -285,7 +301,9 @@ extension PureXML.XSLT { // The loaded document's base URI is the (already base-resolved) // reference it came from, so a relative reference taken from one // of its nodes resolves against it (XSLT 1.0 12.1). - if let tree = cache.trees[path] { recordBaseURI(tree, path, cache) } + if let tree = cache.trees[path] { + recordBaseURI(tree, path, cache) + } } guard let tree = cache.trees[path], let parsed = cache.sources[path] else { return [] } guard let fragment, !fragment.isEmpty else { return [.tree(tree)] } @@ -384,10 +402,18 @@ extension PureXML.Emitting.Options { // leading spaces (indent-amount 0), the conformance golds' form. copy.indent = "" } - if let omit = output.omitXMLDeclaration { copy.includeXMLDeclaration = !omit } - if let encoding = output.encoding { copy.encodingName = encoding } - if let version = output.version { copy.xmlVersion = version } - if let standalone = output.standalone { copy.standalone = standalone } + if let omit = output.omitXMLDeclaration { + copy.includeXMLDeclaration = !omit + } + if let encoding = output.encoding { + copy.encodingName = encoding + } + if let version = output.version { + copy.xmlVersion = version + } + if let standalone = output.standalone { + copy.standalone = standalone + } return copy } } diff --git a/Sources/XSLT/XSLTGlobals.swift b/Sources/XSLT/XSLTGlobals.swift index a4e2638..a0f04eb 100644 --- a/Sources/XSLT/XSLTGlobals.swift +++ b/Sources/XSLT/XSLTGlobals.swift @@ -23,7 +23,9 @@ extension PureXML.XSLT.Transformer { } // A caller-supplied value overrides an xsl:param default and is fixed. for global in globals where stylesheet.parameterNames.contains(global.name) { - if let supplied = parameters[global.name] { variables[global.name] = .string(supplied) } + if let supplied = parameters[global.name] { + variables[global.name] = .string(supplied) + } } for _ in 0 ..< max(1, globals.count) { var changed = false @@ -35,7 +37,9 @@ extension PureXML.XSLT.Transformer { changed = true } } - if !changed { break } + if !changed { + break + } } return variables } diff --git a/Sources/XSLT/XSLTInstantiation.swift b/Sources/XSLT/XSLTInstantiation.swift index 25af4ee..6aa3ee0 100644 --- a/Sources/XSLT/XSLTInstantiation.swift +++ b/Sources/XSLT/XSLTInstantiation.swift @@ -114,7 +114,9 @@ final class XSLTDriver { private func drive() { while let work = stack.popLast() { - if transformer.termination.message != nil || transformer.recursionGuard.exceeded { continue } + if transformer.termination.message != nil || transformer.recursionGuard.exceeded { + continue + } switch work { case let .run(body, cursor, context, sink): stepRun(body, cursor, context, sink) diff --git a/Sources/XSLT/XSLTInstantiationElements.swift b/Sources/XSLT/XSLTInstantiationElements.swift index 082d801..7ef0819 100644 --- a/Sources/XSLT/XSLTInstantiationElements.swift +++ b/Sources/XSLT/XSLTInstantiationElements.swift @@ -202,14 +202,22 @@ extension XSLTDriver { case let .attribute(attribute): // Ignored once content has been added (the recovery for an // attribute created after children). - if children.isEmpty { attributes.append(attribute) } + if children.isEmpty { + attributes.append(attribute) + } case let .node(node): children.append(node) } } into.items.append(.node(.element(.init(name: name, attributes: Host.deduplicated(attributes), children: children)))) case .filteredElement: - into.items.append(contentsOf: child.items.filter { if case .attribute = $0 { false } else { true } }) + into.items.append(contentsOf: child.items.filter { + if case .attribute = $0 { + false + } else { + true + } + }) case .comment: into.items.append(.node(.comment(transformer.escapedTextValue(of: child.items)))) case let .processingInstruction(target): diff --git a/Sources/XSLT/XSLTInstantiationSteps.swift b/Sources/XSLT/XSLTInstantiationSteps.swift index daa60ba..a6dc975 100644 --- a/Sources/XSLT/XSLTInstantiationSteps.swift +++ b/Sources/XSLT/XSLTInstantiationSteps.swift @@ -23,9 +23,15 @@ extension XSLTDriver { } // The remainder of the body runs after this instruction's items. stack.append(.run(body, cursor + 1, context, sink)) - if runSimple(instruction, context, sink) { return } - if runControl(instruction, context, sink) { return } - if runTemplate(instruction, context, sink) { return } + if runSimple(instruction, context, sink) { + return + } + if runControl(instruction, context, sink) { + return + } + if runTemplate(instruction, context, sink) { + return + } runConstruct(instruction, context, sink) } @@ -53,7 +59,9 @@ extension XSLTDriver { private func runControl(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool { switch instruction { case let .ifInstruction(test, thenBody): - if transformer.boolean(test, context) { stack.append(.run(thenBody, 0, context, sink)) } + if transformer.boolean(test, context) { + stack.append(.run(thenBody, 0, context, sink)) + } case let .choose(whens, otherwise): let branch = whens.first { transformer.boolean($0.test, context) }?.body ?? otherwise stack.append(.run(branch, 0, context, sink)) diff --git a/Sources/XSLT/XSLTLookups.swift b/Sources/XSLT/XSLTLookups.swift index 394fcd7..538a7e2 100644 --- a/Sources/XSLT/XSLTLookups.swift +++ b/Sources/XSLT/XSLTLookups.swift @@ -121,7 +121,9 @@ extension PureXML.XSLT.Library { ) { if node.kind == .element, let name = node.name?.description, let idNames = declared[name] { for attribute in node.attributes where idNames.contains(attribute.name.description) { - if index[attribute.value] == nil { index[attribute.value] = node } + if index[attribute.value] == nil { + index[attribute.value] = node + } } } for child in node.children { diff --git a/Sources/XSLT/XSLTMatchCache.swift b/Sources/XSLT/XSLTMatchCache.swift index b223cfa..d0a5dde 100644 --- a/Sources/XSLT/XSLTMatchCache.swift +++ b/Sources/XSLT/XSLTMatchCache.swift @@ -50,7 +50,9 @@ extension PureXML.XSLT { var key = pattern + "\u{1}" + namespaces.sorted(by: { $0.key < $1.key }).map { $0.key + "=" + $0.value }.joined(separator: ";") key += "\u{1}\(UInt(bitPattern: ObjectIdentifier(root).hashValue))" - if let cached = matched[key] { return cached } + if let cached = matched[key] { + return cached + } var result = PatternMatches() for branch in pattern.split(separator: "|") { let trimmed = branch.trimmingXMLWhitespace() diff --git a/Sources/XSLT/XSLTNumbering.swift b/Sources/XSLT/XSLTNumbering.swift index e822e0f..bb4b91f 100644 --- a/Sources/XSLT/XSLTNumbering.swift +++ b/Sources/XSLT/XSLTNumbering.swift @@ -16,7 +16,9 @@ enum XSLTNumbering { matches: (Tree, String) -> Bool, ) -> [Int] { func matchesCount(_ candidate: Tree) -> Bool { - if let count { return matches(candidate, count) } + if let count { + return matches(candidate, count) + } return candidate.kind == node.kind && candidate.name?.description == node.name?.description } func matchesFrom(_ candidate: Tree) -> Bool { @@ -44,8 +46,12 @@ enum XSLTNumbering { case "any": var total = 0 walkPreceding(node) { candidate in - if matchesFrom(candidate) { return false } - if matchesCount(candidate) { total += 1 } + if matchesFrom(candidate) { + return false + } + if matchesCount(candidate) { + total += 1 + } return true } return [total] @@ -57,7 +63,9 @@ enum XSLTNumbering { if matchesCount(candidate) { return [position(of: candidate)] } - if matchesFrom(candidate) { break } + if matchesFrom(candidate) { + break + } current = candidate.parent } return [] @@ -80,8 +88,12 @@ enum XSLTNumbering { } var position = 1 for sibling in parent.children { - if sibling === node { break } - if matchesCount(sibling) { position += 1 } + if sibling === node { + break + } + if matchesCount(sibling) { + position += 1 + } } return position } @@ -114,7 +126,9 @@ enum XSLTNumbering { private static func walkPreceding(_ node: Tree, _ visit: (Tree) -> Bool) { var current: Tree? = node while let candidate = current { - if !visit(candidate) { return } + if !visit(candidate) { + return + } if let parent = candidate.parent { // The preceding siblings, nearest first. Find the candidate's // index in the child list once, then walk the children before it @@ -154,8 +168,12 @@ enum XSLTNumbering { } let token = offset < pieces.tokens.count ? pieces.tokens[offset] : lastToken result += render(number, token, grouping) - if offset < pieces.tokens.count { lastToken = pieces.tokens[offset] } - if offset > 0, offset - 1 < pieces.separators.count { lastSeparator = pieces.separators[offset - 1] } + if offset < pieces.tokens.count { + lastToken = pieces.tokens[offset] + } + if offset > 0, offset - 1 < pieces.separators.count { + lastSeparator = pieces.separators[offset - 1] + } } return result + pieces.suffix } @@ -213,7 +231,9 @@ enum XSLTNumbering { default: var digits = String(number) let width = token.count - if digits.count < width { digits = String(repeating: "0", count: width - digits.count) + digits } + if digits.count < width { + digits = String(repeating: "0", count: width - digits.count) + digits + } return grouped(digits, grouping) } } diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift index 3599599..8038c41 100644 --- a/Sources/XSLT/XSLTParser.swift +++ b/Sources/XSLT/XSLTParser.swift @@ -233,12 +233,18 @@ extension PureXML.XSLT { /// before the node test is classified (`attribute::node()` is -0.5, like /// `node()`, not 0). static func defaultPriority(_ pattern: String) -> Double { - if pattern.contains("/") || pattern.contains("[") { return 0.5 } + if pattern.contains("/") || pattern.contains("[") { + return 0.5 + } let test = pattern.hasPrefix("attribute::") ? String(pattern.dropFirst(11)) : pattern.hasPrefix("child::") ? String(pattern.dropFirst(7)) : pattern - if ["*", "@*", "node()", "text()", "comment()", "processing-instruction()"].contains(test) { return -0.5 } - if test.hasSuffix(":*") { return -0.25 } + if ["*", "@*", "node()", "text()", "comment()", "processing-instruction()"].contains(test) { + return -0.5 + } + if test.hasSuffix(":*") { + return -0.25 + } return 0 } @@ -254,7 +260,9 @@ extension PureXML.XSLT { var result: [Instruction] = [] var run = "" func flush() { - if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) { result.append(.literalText(run)) } + if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) { + result.append(.literalText(run)) + } run = "" } for child in node.children { @@ -262,7 +270,9 @@ extension PureXML.XSLT { run += child.value } else { flush() - if let instruction = instruction(child) { result.append(instruction) } + if let instruction = instruction(child) { + result.append(instruction) + } } } flush() @@ -282,7 +292,9 @@ extension PureXML.XSLT { } private static func xslInstruction(_ node: XSLTTree) -> Instruction? { - if let known = simpleInstruction(node) ?? structuralInstruction(node) { return known } + if let known = simpleInstruction(node) ?? structuralInstruction(node) { + return known + } // An unrecognized XSLT element instantiates its xsl:fallback children // (forwards-compatible processing); with none, it is dropped. let fallback = XSLTNode.children(node, named: "fallback").flatMap(body) diff --git a/Sources/XSLT/XSLTParserDeclarations.swift b/Sources/XSLT/XSLTParserDeclarations.swift index bac4632..f4b4ef6 100644 --- a/Sources/XSLT/XSLTParserDeclarations.swift +++ b/Sources/XSLT/XSLTParserDeclarations.swift @@ -19,7 +19,9 @@ extension PureXML.XSLT.XSLTParser { for attribute in element.attributes { let isDefault = prefix == "#default" && attribute.name.prefix == nil && attribute.name.localName == "xmlns" let isPrefixed = attribute.name.prefix == "xmlns" && attribute.name.localName == prefix - if isDefault || isPrefixed { return attribute.value } + if isDefault || isPrefixed { + return attribute.value + } } current = element.parent } @@ -95,7 +97,9 @@ extension PureXML.XSLT.XSLTParser { static func decimalFormat(_ node: XSLTTree) -> PureXML.XSLT.DecimalFormat { var format = PureXML.XSLT.DecimalFormat() func char(_ name: String, _ keyPath: WritableKeyPath) { - if let value = XSLTNode.attribute(node, name)?.first { format[keyPath: keyPath] = value } + if let value = XSLTNode.attribute(node, name)?.first { + format[keyPath: keyPath] = value + } } char("decimal-separator", \.decimalSeparator) char("grouping-separator", \.groupingSeparator) @@ -105,8 +109,12 @@ extension PureXML.XSLT.XSLTParser { char("digit", \.digit) char("pattern-separator", \.patternSeparator) char("minus-sign", \.minusSign) - if let infinity = XSLTNode.attribute(node, "infinity") { format.infinity = infinity } - if let notANumber = XSLTNode.attribute(node, "NaN") { format.notANumber = notANumber } + if let infinity = XSLTNode.attribute(node, "infinity") { + format.infinity = infinity + } + if let notANumber = XSLTNode.attribute(node, "NaN") { + format.notANumber = notANumber + } return format } } diff --git a/Sources/XSLT/XSLTParserEntry.swift b/Sources/XSLT/XSLTParserEntry.swift index 1e45911..bf5eb1d 100644 --- a/Sources/XSLT/XSLTParserEntry.swift +++ b/Sources/XSLT/XSLTParserEntry.swift @@ -99,7 +99,9 @@ extension PureXML.XSLT.XSLTParser { guard let head = needle.first else { return true } var search = text[...] while let start = search.firstIndex(of: head) { - if search[start...].hasPrefix(needle) { return true } + if search[start...].hasPrefix(needle) { + return true + } search = search[search.index(after: start)...] } return false diff --git a/Sources/XSLT/XSLTParserStructural.swift b/Sources/XSLT/XSLTParserStructural.swift index 23b4511..8358086 100644 --- a/Sources/XSLT/XSLTParserStructural.swift +++ b/Sources/XSLT/XSLTParserStructural.swift @@ -69,7 +69,9 @@ extension PureXML.XSLT.XSLTParser { var excludedURIs: Set = [XSLTNode.namespace] for prefix in excludedPrefixTokens(node) { let key = prefix == "#default" ? "" : prefix - if let uri = bindings[key] { excludedURIs.insert(uri) } + if let uri = bindings[key] { + excludedURIs.insert(uri) + } } return bindings.filter { !excludedURIs.contains($0.value) && !($0.key.isEmpty && $0.value.isEmpty) } } diff --git a/Sources/XSLT/XSLTSorting.swift b/Sources/XSLT/XSLTSorting.swift index 2a15f93..108bb95 100644 --- a/Sources/XSLT/XSLTSorting.swift +++ b/Sources/XSLT/XSLTSorting.swift @@ -38,7 +38,9 @@ extension PureXML.XSLT.Transformer { } else { order = Self.compareKeys(lhs.keys[index], rhs.keys[index], sort) } - if order != 0 { return order < 0 } + if order != 0 { + return order < 0 + } } return lhs.offset < rhs.offset }.map(\.node) @@ -76,7 +78,9 @@ extension PureXML.XSLT.Transformer { } let leftSpaces = spacePositions(left) let rightSpaces = spacePositions(right) - if leftSpaces == rightSpaces { return 0 } + if leftSpaces == rightSpaces { + return 0 + } for (leftIndex, rightIndex) in zip(leftSpaces, rightSpaces) where leftIndex != rightIndex { return leftIndex > rightIndex ? -1 : 1 } diff --git a/Sources/XSLT/XSLTTransform.swift b/Sources/XSLT/XSLTTransform.swift index 73633b8..4d88747 100644 --- a/Sources/XSLT/XSLTTransform.swift +++ b/Sources/XSLT/XSLTTransform.swift @@ -38,8 +38,12 @@ public extension PureXML.XSLT { maxTemplateDepth: maxTemplateDepth, ) let result = transformer.run() - if transformer.recursionLimitExceeded { throw XSLTError.recursionLimitExceeded(maxTemplateDepth) } - if let message = transformer.terminationMessage { throw XSLTError.terminated(message) } + if transformer.recursionLimitExceeded { + throw XSLTError.recursionLimitExceeded(maxTemplateDepth) + } + if let message = transformer.terminationMessage { + throw XSLTError.terminated(message) + } let method = sheet.output.method ?? defaultMethod(for: result) if method == "text" { // Text output is never escaped, so raw markers are only removed, @@ -133,7 +137,9 @@ public extension PureXML.XSLT { let wrap = names.contains(element.name.localName) || names.contains(element.name.description) let children = wrap ? rebuiltChildren.flatMap { child -> [PureXML.Model.Node] in - if case let .text(value) = child { return cdataNodes(value) } + if case let .text(value) = child { + return cdataNodes(value) + } return [child] } : rebuiltChildren @@ -174,7 +180,9 @@ public extension PureXML.XSLT { return first ..< end } start = haystack.index(after: first) - if start >= haystack.endIndex { break } + if start >= haystack.endIndex { + break + } } return nil } diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift index 99c92d0..96591fe 100644 --- a/Sources/XSLT/XSLTTransformer.swift +++ b/Sources/XSLT/XSLTTransformer.swift @@ -74,7 +74,9 @@ extension PureXML.XSLT { current: .tree(root), keys: { documentRoot in let identity = ObjectIdentifier(documentRoot) - if let cached = caches.indexes[identity] { return cached } + if let cached = caches.indexes[identity] { + return cached + } let built = PureXML.XSLT.Library.buildKeyIndex(stylesheet: stylesheet, root: documentRoot) caches.indexes[identity] = built return built @@ -91,7 +93,9 @@ extension PureXML.XSLT { /// per document: the source and each document() load have their own). private func keyIndex(for documentRoot: PureXML.Model.TreeNode) -> PureXML.XSLT.KeyIndex { let identity = ObjectIdentifier(documentRoot) - if let cached = keyIndexes.indexes[identity] { return cached } + if let cached = keyIndexes.indexes[identity] { + return cached + } let built = PureXML.XSLT.Library.buildKeyIndex(stylesheet: stylesheet, root: documentRoot) keyIndexes.indexes[identity] = built return built @@ -145,10 +149,14 @@ extension PureXML.XSLT { } func variableValue(_ select: String?, _ body: [Instruction], _ context: XSLTContext) -> PureXML.XPath.Value { - if let select { return value(select, context) ?? .string("") } + if let select { + return value(select, context) ?? .string("") + } // XSLT 1.0 11.2: a variable with no select and empty content is the // empty string, not an RTF (so boolean() is false); only content is. - if body.isEmpty { return .string("") } + if body.isEmpty { + return .string("") + } // A body variable is a result-tree fragment: a queryable document node. let children = instantiate(body, context).compactMap(Self.nodeOf).map(PureXML.Model.TreeNode.init) return .nodeSet([.tree(PureXML.Model.TreeNode.document(children: children))]) @@ -203,13 +211,17 @@ extension PureXML.XSLT { } fileprivate static func nodeOf(_ item: ResultItem) -> PureXML.Model.Node? { - if case let .node(node) = item { return node } + if case let .node(node) = item { + return node + } return nil } static func text(of items: [ResultItem]) -> String { items.reduce(into: "") { result, item in - if case let .node(node) = item { result += Self.stringValue(node) } + if case let .node(node) = item { + result += Self.stringValue(node) + } } } @@ -244,7 +256,9 @@ extension PureXML.XSLT.Transformer { for definition in stylesheet.attributeSets[name] ?? [] { result += attributeSetAttributes(definition.use, context, visiting: visiting.union([name])) for item in instantiate(definition.attributes, context) { - if case let .attribute(attribute) = item { result.append(attribute) } + if case let .attribute(attribute) = item { + result.append(attribute) + } } } } diff --git a/Sources/XSLT/XSLTWhitespace.swift b/Sources/XSLT/XSLTWhitespace.swift index 1d67649..d765556 100644 --- a/Sources/XSLT/XSLTWhitespace.swift +++ b/Sources/XSLT/XSLTWhitespace.swift @@ -38,8 +38,12 @@ extension PureXML.XSLT { let expanded = uri.isEmpty ? local : "{\(uri)}\(local)" let namespaceWildcard = uri.isEmpty ? nil : "{\(uri)}*" for test in [expanded, namespaceWildcard, "*"].compactMap(\.self) { - if preserveSet.contains(test) { return false } - if stripSet.contains(test) { return true } + if preserveSet.contains(test) { + return false + } + if stripSet.contains(test) { + return true + } } return false } diff --git a/Tests/AttributeDocumentOrderTests.swift b/Tests/AttributeDocumentOrderTests.swift index 6be7061..09231f5 100644 --- a/Tests/AttributeDocumentOrderTests.swift +++ b/Tests/AttributeDocumentOrderTests.swift @@ -11,7 +11,9 @@ import Testing struct AttributeDocumentOrderTests { private func values(_ path: String, _ xml: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(xml)).compactMap { - if case let .attribute(attribute) = $0 { return attribute.value } + if case let .attribute(attribute) = $0 { + return attribute.value + } return nil } } diff --git a/Tests/ByteFastPathTests.swift b/Tests/ByteFastPathTests.swift index cf09d77..ad2a8b0 100644 --- a/Tests/ByteFastPathTests.swift +++ b/Tests/ByteFastPathTests.swift @@ -150,7 +150,9 @@ private extension PureXML.Model.Node { var firstElement: PureXML.Model.Element? { guard case let .document(children) = self else { return element } for child in children { - if case let .element(element) = child { return element } + if case let .element(element) = child { + return element + } } return nil } diff --git a/Tests/ContentModelOracle.swift b/Tests/ContentModelOracle.swift index 4f01ec7..d480fd3 100644 --- a/Tests/ContentModelOracle.swift +++ b/Tests/ContentModelOracle.swift @@ -45,7 +45,9 @@ enum ContentModelOracle { next.formUnion(ends(part, tokens, position)) } positions = next - if positions.isEmpty { break } + if positions.isEmpty { + break + } } return positions case let .choice(parts): @@ -75,10 +77,14 @@ enum ContentModelOracle { byReps.append(next) reps += 1 let priorPositions = byReps[0 ..< reps].reduce(into: Set()) { $0.formUnion($1) } - if next.isSubset(of: priorPositions), reps >= low { break } + if next.isSubset(of: priorPositions), reps >= low { + break + } } var out: Set = [] - if low == 0 { out.insert(start) } + if low == 0 { + out.insert(start) + } for count in low ... cap where count < byReps.count { out.formUnion(byReps[count]) } @@ -89,7 +95,9 @@ enum ContentModelOracle { /// `xs:all` group in any order; a member that is nullable may be skipped. private static func allEnds(_ members: [ContentModel], _ done: [Bool], _ tokens: [String], _ start: Int) -> Set { var out: Set = [] - if members.indices.allSatisfy({ done[$0] || nullable(members[$0]) }) { out.insert(start) } + if members.indices.allSatisfy({ done[$0] || nullable(members[$0]) }) { + out.insert(start) + } for index in members.indices where !done[index] { for end in ends(members[index], tokens, start) where end > start { var next = done diff --git a/Tests/ContentNFAStateBudgetTests.swift b/Tests/ContentNFAStateBudgetTests.swift index ca5c52f..7024ee3 100644 --- a/Tests/ContentNFAStateBudgetTests.swift +++ b/Tests/ContentNFAStateBudgetTests.swift @@ -84,7 +84,9 @@ struct ContentNFAStateBudgetTests { let follow = nfa.follow(after: names("a", "a")) let allowedNames = follow.allowed.compactMap { label -> String? in - if case let .name(name) = label { return name.localName } + if case let .name(name) = label { + return name.localName + } return nil } diff --git a/Tests/DTDSubsetStrictnessTests.swift b/Tests/DTDSubsetStrictnessTests.swift index 0dd7fd4..179d738 100644 --- a/Tests/DTDSubsetStrictnessTests.swift +++ b/Tests/DTDSubsetStrictnessTests.swift @@ -147,7 +147,11 @@ struct DTDSubsetStrictnessTests { case let .element(root)? = children.first(where: { $0.element != nil }) else { return } #expect(root.children.allSatisfy { child in - if case let .text(value) = child { value == "ok" } else { false } + if case let .text(value) = child { + value == "ok" + } else { + false + } }) } } diff --git a/Tests/DTDTests.swift b/Tests/DTDTests.swift index 362e4d1..69b2c71 100644 --- a/Tests/DTDTests.swift +++ b/Tests/DTDTests.swift @@ -43,7 +43,9 @@ struct DTDTests { func test_recursiveEntity() { let xml = "]>&a;" expectError(xml, limits: dtdAllowed) { error in - if case .recursiveEntity = error { return true } + if case .recursiveEntity = error { + return true + } return false } } @@ -57,7 +59,9 @@ struct DTDTests { + "]>&l2;" let limits = PureXML.Parsing.Limits(allowDoctype: true, maxEntityExpansion: 100) expectError(xml, limits: limits) { error in - if case .amplificationLimitExceeded = error { return true } + if case .amplificationLimitExceeded = error { + return true + } return false } } @@ -68,7 +72,9 @@ struct DTDTests { // rather than reading the file: external resolution never happens. let xml = "]>&ext;" expectError(xml, limits: dtdAllowed) { error in - if case .undefinedEntity = error { return true } + if case .undefinedEntity = error { + return true + } return false } } @@ -91,7 +97,9 @@ struct DTDTests { private func rootElement(_ node: PureXML.Model.Node) -> PureXML.Model.Element? { guard case let .document(children) = node else { return nil } for child in children { - if case let .element(element) = child { return element } + if case let .element(element) = child { + return element + } } return nil } diff --git a/Tests/DeterminismDifferentialTests.swift b/Tests/DeterminismDifferentialTests.swift index 3d51c1b..4d76067 100644 --- a/Tests/DeterminismDifferentialTests.swift +++ b/Tests/DeterminismDifferentialTests.swift @@ -79,7 +79,9 @@ struct DeterminismDifferentialTests { let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() let output = String(decoding: data, as: UTF8.self) - if process.terminationStatus == 0 { return .accepts } + if process.terminationStatus == 0 { + return .accepts + } return output.contains("not determinist") ? .notDeterminist : .otherReject #endif } @@ -98,12 +100,18 @@ struct DeterminismDifferentialTests { guard let reference = libxml2Verdict(source, xmllint: xmllint) else { continue } // Only the determinism dimension: skip when either side rejects for an // unrelated reason (a structural error the other may or may not share). - if pureXML == .otherReject || reference == .otherReject { continue } + if pureXML == .otherReject || reference == .otherReject { + continue + } compared += 1 if pureXML == .notDeterminist, reference == .accepts { - if falsePositives.count < 20 { falsePositives.append(source) } + if falsePositives.count < 20 { + falsePositives.append(source) + } } else if pureXML == .accepts, reference == .notDeterminist { - if falseNegatives.count < 20 { falseNegatives.append(source) } + if falseNegatives.count < 20 { + falseNegatives.append(source) + } } } let report = "compared \(compared) schemas; false positives \(falsePositives.count), false negatives \(falseNegatives.count)" @@ -152,7 +160,9 @@ private struct DeterminismSchemaGenerator { private mutating func particle(depth: Int) -> String { // Leaf: an element of a small alphabet, or a wildcard. if depth == 0 || chance(2) { - if chance(4) { return "" } + if chance(4) { + return "" + } return "" } // A reference to one of the defined groups (the multiply-referenced overlap). diff --git a/Tests/EncodingTests.swift b/Tests/EncodingTests.swift index abb6a53..57a018e 100644 --- a/Tests/EncodingTests.swift +++ b/Tests/EncodingTests.swift @@ -296,7 +296,9 @@ struct EncodingTests { private func rootText(_ node: PureXML.Model.Node) -> String? { guard case let .document(children) = node else { return nil } for child in children { - if case let .element(element) = child { return element.text } + if case let .element(element) = child { + return element.text + } } return nil } diff --git a/Tests/EntityDecodeBulkTests.swift b/Tests/EntityDecodeBulkTests.swift index ce66a11..aa73725 100644 --- a/Tests/EntityDecodeBulkTests.swift +++ b/Tests/EntityDecodeBulkTests.swift @@ -82,7 +82,13 @@ struct EntityDecodeBulkTests { Issue.record("no root") return } - let text = root.children.compactMap { if case let .text(value) = $0 { value } else { nil } }.joined() + let text = root.children.compactMap { + if case let .text(value) = $0 { + value + } else { + nil + } + }.joined() #expect(text == "x & y < z > w") } } diff --git a/Tests/FacetValidationDifferentialTests.swift b/Tests/FacetValidationDifferentialTests.swift index a52cef7..45553a5 100644 --- a/Tests/FacetValidationDifferentialTests.swift +++ b/Tests/FacetValidationDifferentialTests.swift @@ -68,7 +68,9 @@ struct FacetValidationDifferentialTests { let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() let output = String(decoding: data, as: UTF8.self) - if output.contains("failed to compile") || output.contains("Schemas parser error") { return .schemaInvalid } + if output.contains("failed to compile") || output.contains("Schemas parser error") { + return .schemaInvalid + } return process.terminationStatus == 0 ? .valid : .invalid #endif } @@ -99,12 +101,18 @@ struct FacetValidationDifferentialTests { let instance = "\(value)" let pureValid = ((try? schema.document.validate(instance)) ?? [PureXML.Validation.ValidationError]()).isEmpty guard let reference = libxml2(schema.file, instance, xmllint: xmllint) else { continue } - if reference == .schemaInvalid { return } // libxml2 rejects the schema; skip its values + if reference == .schemaInvalid { + return + } // libxml2 rejects the schema; skip its values tally.compared += 1 let detail = "\(schema.source)\n value: '\(value)' (PureXML \(pureValid ? "valid" : "invalid"), libxml2 \(reference))" switch compare(pureValid: pureValid, reference: reference) { - case .falsePositive: if tally.falsePositives.count < 25 { tally.falsePositives.append(detail) } - case .falseNegative: if tally.falseNegatives.count < 25 { tally.falseNegatives.append(detail) } + case .falsePositive: if tally.falsePositives.count < 25 { + tally.falsePositives.append(detail) + } + case .falseNegative: if tally.falseNegatives.count < 25 { + tally.falseNegatives.append(detail) + } case .agreeValid: tally.agreeValid += 1 case .agreeInvalid: tally.agreeInvalid += 1 } @@ -244,9 +252,13 @@ private struct FacetSchemaGenerator { var chosen: [(name: String, value: String)] = [] var seenName: Set = [] for facet in current.facets where Int.random(in: 0 ..< 2, using: &rng) == 0 { - if facet.name == "enumeration" || seenName.insert(facet.name).inserted { chosen.append(facet) } + if facet.name == "enumeration" || seenName.insert(facet.name).inserted { + chosen.append(facet) + } + } + if chosen.isEmpty { + chosen = [pick(current.facets)] } - if chosen.isEmpty { chosen = [pick(current.facets)] } let restriction = chosen.map { "" }.joined() return "" + "" diff --git a/Tests/Fixtures/validation-field-registry.txt b/Tests/Fixtures/validation-field-registry.txt index a487b68..a4cb5c0 100644 --- a/Tests/Fixtures/validation-field-registry.txt +++ b/Tests/Fixtures/validation-field-registry.txt @@ -18,20 +18,20 @@ FIELD parseAdvisories dtdParseAdvisories IGNORE isEmpty derived summary; no independent constraint MODEL PureXML.Validation.XSDContext -FIELD types xsdContentValidity,xsdStreamingShallowValidity -FIELD constraints xsdIdentityConstraints -FIELD rootDeclaration xsdContentValidity -FIELD nillableElements xsdContentValidity -FIELD elementConstraints xsdContentValidity -FIELD abstractTypes xsdContentValidity -FIELD typeBlock xsdContentValidity -FIELD elementBlock xsdContentValidity -FIELD typeDerivation xsdContentValidity +FIELD types xsdTypeReferenceResolves,xsdContentModelStructure,xsdXsiTypeSubstitutionValid,xsdXsiTypeOverrideResolves,xsdStreamingShallowValidity +FIELD constraints xsdIdentityConstraintXPathsValid,xsdIdentityKeyFieldPresent,xsdIdentityValueUnique,xsdIdentityKeyrefResolves,xsdIdentityFieldCardinality,xsdIdentityFieldMustBeSimple +FIELD rootDeclaration xsdContentModelStructure +FIELD nillableElements xsdNilAttributeRequiresNillable +FIELD elementConstraints xsdNilElementNoFixedConstraint,xsdFixedElementNoChildren,xsdFixedElementValueMatches +FIELD abstractTypes xsdXsiTypeSubstitutionValid +FIELD typeBlock xsdXsiTypeSubstitutionValid +FIELD elementBlock xsdXsiTypeSubstitutionValid +FIELD typeDerivation xsdXsiTypeSubstitutionValid MODEL PureXML.Model.Element -FIELD name dtdContentModel,dtdUndeclaredElement,dtdUndeclaredAttributes,htmlRequiredParent,htmlVoidElementsAreEmpty,xsdContentValidity +FIELD name dtdContentModel,dtdUndeclaredElement,dtdUndeclaredAttributes,htmlRequiredParent,htmlVoidElementsAreEmpty,xsdContentModelStructure FIELD attributes uniqueAttributes,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredAttributes,dtdStandaloneAttributes -FIELD children dtdContentModel,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentValidity +FIELD children dtdContentModel,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentModelStructure IGNORE text derived from direct text and CDATA children MODEL PureXML.Model.Attribute @@ -39,8 +39,8 @@ FIELD name uniqueAttributes,dtdUndeclaredAttributes,dtdRequiredAttributes,dtdFix FIELD value dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdIdentifierIntegrity MODEL PureXML.Model.Node -CASE document dtdDeclarationValidity,dtdRootElementType,dtdIdentifierIntegrity,htmlUniqueIdentifiers,xsdContentValidity,xsdIdentityConstraints -CASE element uniqueAttributes,dtdContentModel,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredElement,dtdUndeclaredAttributes,dtdStandaloneAttributes,dtdStandaloneElementWhitespace,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentValidity +CASE document dtdDeclarationValidity,dtdRootElementType,dtdIdentifierIntegrity,htmlUniqueIdentifiers,xsdContentModelStructure,xsdIdentityConstraintXPathsValid +CASE element uniqueAttributes,dtdContentModel,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredElement,dtdUndeclaredAttributes,dtdStandaloneAttributes,dtdStandaloneElementWhitespace,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentModelStructure CASE text dtdContentModel CASE cdata dtdContentModel IGNORE comment no DTD or XSD constraints on comments diff --git a/Tests/FollowingPrecedingAxisTests.swift b/Tests/FollowingPrecedingAxisTests.swift index abe2149..1733e0b 100644 --- a/Tests/FollowingPrecedingAxisTests.swift +++ b/Tests/FollowingPrecedingAxisTests.swift @@ -12,7 +12,9 @@ import Testing struct FollowingPrecedingAxisTests { private func names(_ path: String, _ xml: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(xml)).compactMap { - if case let .node(node) = $0 { return node.element?.name.localName } + if case let .node(node) = $0 { + return node.element?.name.localName + } return nil } } diff --git a/Tests/HTMLForeignContentTests.swift b/Tests/HTMLForeignContentTests.swift index 73e0e4d..002c5c0 100644 --- a/Tests/HTMLForeignContentTests.swift +++ b/Tests/HTMLForeignContentTests.swift @@ -16,7 +16,9 @@ struct HTMLForeignContentTests { case let .document(children): return children.lazy.compactMap { find(name, in: $0) }.first case let .element(element): - if element.name.localName == name { return element } + if element.name.localName == name { + return element + } return element.children.lazy.compactMap { find(name, in: $0) }.first default: return nil diff --git a/Tests/HTMLTokenizerTests.swift b/Tests/HTMLTokenizerTests.swift index ba6509c..78af6af 100644 --- a/Tests/HTMLTokenizerTests.swift +++ b/Tests/HTMLTokenizerTests.swift @@ -6,7 +6,13 @@ struct HTMLTokenizerTests { /// The concatenated text content of the first parsed element. private func text(_ html: String) -> String { guard case let .document(children) = PureXML.HTML.parse(html), case let .element(element) = children.first else { return "" } - return element.children.compactMap { if case let .text(value) = $0 { value } else { nil } }.joined() + return element.children.compactMap { + if case let .text(value) = $0 { + value + } else { + nil + } + }.joined() } @Test("A numeric reference to zero, a surrogate, or an out-of-range value yields U+FFFD") diff --git a/Tests/InstanceValidationDifferentialTests.swift b/Tests/InstanceValidationDifferentialTests.swift index 2ab2ae5..5cafd40 100644 --- a/Tests/InstanceValidationDifferentialTests.swift +++ b/Tests/InstanceValidationDifferentialTests.swift @@ -46,7 +46,9 @@ struct InstanceValidationDifferentialTests { let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() let output = String(decoding: data, as: UTF8.self) - if output.contains("failed to compile") || output.contains("Schemas parser error") { return .schemaInvalid } + if output.contains("failed to compile") || output.contains("Schemas parser error") { + return .schemaInvalid + } return process.terminationStatus == 0 ? .valid : .invalid #endif } @@ -87,14 +89,22 @@ struct InstanceValidationDifferentialTests { let instance = generator.instance() let pureValid = ((try? schema.document.validate(instance.xml)) ?? [PureXML.Validation.ValidationError]()).isEmpty guard let reference = libxml2(schema.file, instance.xml, xmllint: xmllint) else { continue } - if reference == .schemaInvalid { return } // libxml2 rejects the schema; skip its instances + if reference == .schemaInvalid { + return + } // libxml2 rejects the schema; skip its instances let truth = ContentModelOracle.matches(schema.model, instance.tokens) tally.compared += 1 - if (reference == .valid) != truth { tally.libxml2Disagreements += 1 } + if (reference == .valid) != truth { + tally.libxml2Disagreements += 1 + } let detail = "\(schema.source)\n instance: \(instance.xml)\n oracle: \(truth ? "valid" : "invalid"), libxml2: \(reference)" switch (pureValid, truth) { - case (false, true): if tally.falsePositives.count < 20 { tally.falsePositives.append(detail) } - case (true, false): if tally.falseNegatives.count < 20 { tally.falseNegatives.append(detail) } + case (false, true): if tally.falsePositives.count < 20 { + tally.falsePositives.append(detail) + } + case (true, false): if tally.falseNegatives.count < 20 { + tally.falseNegatives.append(detail) + } case (true, true): tally.agreeValid += 1 case (false, false): tally.agreeInvalid += 1 } diff --git a/Tests/LineEndingTests.swift b/Tests/LineEndingTests.swift index 1cd1503..a87b26f 100644 --- a/Tests/LineEndingTests.swift +++ b/Tests/LineEndingTests.swift @@ -9,7 +9,9 @@ struct LineEndingTests { return "" } return root.children.reduce(into: "") { result, child in - if case let .text(text) = child { result += text } + if case let .text(text) = child { + result += text + } } } diff --git a/Tests/NamespaceTests.swift b/Tests/NamespaceTests.swift index 14a7cd4..f66d1c3 100644 --- a/Tests/NamespaceTests.swift +++ b/Tests/NamespaceTests.swift @@ -52,7 +52,9 @@ struct NamespaceTests { let root = try rootElement("") #expect(root.name.namespaceURI == "http://1") let children = root.children.compactMap { node -> PureXML.Model.Element? in - if case let .element(element) = node { return element } + if case let .element(element) = node { + return element + } return nil } #expect(children.first?.name.namespaceURI == "http://2") @@ -65,14 +67,18 @@ struct NamespaceTests { throw Failure.notDocument } for child in children { - if case let .element(element) = child { return element } + if case let .element(element) = child { + return element + } } throw Failure.noElement } private func firstChildElement(_ element: PureXML.Model.Element) -> PureXML.Model.Element? { for child in element.children { - if case let .element(found) = child { return found } + if case let .element(found) = child { + return found + } } return nil } diff --git a/Tests/OutputEncodingTests.swift b/Tests/OutputEncodingTests.swift index d4de99d..6e4bd65 100644 --- a/Tests/OutputEncodingTests.swift +++ b/Tests/OutputEncodingTests.swift @@ -6,7 +6,9 @@ struct OutputEncodingTests { private func rootText(_ node: PureXML.Model.Node) -> String? { guard case let .document(children) = node else { return nil } for child in children { - if case let .element(element) = child { return element.text } + if case let .element(element) = child { + return element.text + } } return nil } diff --git a/Tests/ParsingTests.swift b/Tests/ParsingTests.swift index 4475922..80cbdfd 100644 --- a/Tests/ParsingTests.swift +++ b/Tests/ParsingTests.swift @@ -178,7 +178,9 @@ private extension PureXML.Model.Node { var firstElement: PureXML.Model.Element? { guard case let .document(children) = self else { return element } for child in children { - if case let .element(element) = child { return element } + if case let .element(element) = child { + return element + } } return nil } diff --git a/Tests/PushParserTests.swift b/Tests/PushParserTests.swift index c311765..427cd95 100644 --- a/Tests/PushParserTests.swift +++ b/Tests/PushParserTests.swift @@ -91,7 +91,9 @@ struct PushParserTests { } var names: [String] = [] for try await event in PureXML.events(feeding: stream) { - if case let .startElement(name, _) = event { names.append(name.description) } + if case let .startElement(name, _) = event { + names.append(name.description) + } } #expect(names == ["doc", "item", "item"]) } diff --git a/Tests/RelaxNGSpecSuiteTests.swift b/Tests/RelaxNGSpecSuiteTests.swift index 4ec739b..cb7fcae 100644 --- a/Tests/RelaxNGSpecSuiteTests.swift +++ b/Tests/RelaxNGSpecSuiteTests.swift @@ -116,7 +116,13 @@ struct RelaxNGSpecSuiteTests { for child in testCase.children.compactMap(\.element) { switch child.name.localName { case "section": - section = child.children.compactMap { if case let .text(value) = $0 { value } else { nil } }.joined() + section = child.children.compactMap { + if case let .text(value) = $0 { + value + } else { + nil + } + }.joined() case "resource", "dir": merge(child, prefix: "", into: &scope) case "correct", "incorrect": diff --git a/Tests/SAXTests.swift b/Tests/SAXTests.swift index 3a6ebc2..5e07831 100644 --- a/Tests/SAXTests.swift +++ b/Tests/SAXTests.swift @@ -33,7 +33,9 @@ struct SAXTests { func test_namespace() throws { var uri: String? let handler = PureXML.Parsing.SAXHandler(startElement: { name, _ in - if name.localName == "a" { uri = name.namespaceURI } + if name.localName == "a" { + uri = name.namespaceURI + } }) try PureXML.parse("", sax: handler) #expect(uri == "http://x") diff --git a/Tests/SchemaDifferentialHarness.swift b/Tests/SchemaDifferentialHarness.swift index d180998..3484591 100644 --- a/Tests/SchemaDifferentialHarness.swift +++ b/Tests/SchemaDifferentialHarness.swift @@ -69,10 +69,18 @@ enum SchemaDifferential { } private static func libxml2CompiledSchema(output: String, exitCode: Int32, schemaFile: String) -> Bool { - if output.contains("WXS schema \(schemaFile) failed to compile") { return false } - if output.contains("Schemas parser error"), output.contains(schemaFile) { return false } - if output.contains("Failed to parse the XML resource '\(schemaFile)'") { return false } - if exitCode == 5 { return false } + if output.contains("WXS schema \(schemaFile) failed to compile") { + return false + } + if output.contains("Schemas parser error"), output.contains(schemaFile) { + return false + } + if output.contains("Failed to parse the XML resource '\(schemaFile)'") { + return false + } + if exitCode == 5 { + return false + } if exitCode == 1, output.localizedCaseInsensitiveContains("parser error"), output.contains(schemaFile) { return false } @@ -101,7 +109,9 @@ enum SchemaDifferentialXSTS { found += manifestElements(named: name, under: child) } case let .element(element): - if element.name.localName == name { found.append(element) } + if element.name.localName == name { + found.append(element) + } for child in element.children { found += manifestElements(named: name, under: child) } @@ -135,7 +145,9 @@ enum SchemaDifferentialXSTS { var parts = base.split(separator: "/", omittingEmptySubsequences: false).map(String.init) for segment in href.split(separator: "/") { if segment == ".." { - if !parts.isEmpty { parts.removeLast() } + if !parts.isEmpty { + parts.removeLast() + } } else if segment != "." { parts.append(String(segment)) } diff --git a/Tests/SchemaXSTSDiagnosticTests.swift b/Tests/SchemaXSTSDiagnosticTests.swift index a474551..2e8d36f 100644 --- a/Tests/SchemaXSTSDiagnosticTests.swift +++ b/Tests/SchemaXSTSDiagnosticTests.swift @@ -83,7 +83,9 @@ struct SchemaXSTSDiagnosticTests { found += elements(named: name, under: child) } case let .element(element): - if element.name.localName == name { found.append(element) } + if element.name.localName == name { + found.append(element) + } for child in element.children { found += elements(named: name, under: child) } @@ -110,7 +112,13 @@ struct SchemaXSTSDiagnosticTests { private func resolve(_ href: String, against base: String) -> String { var parts = base.split(separator: "/", omittingEmptySubsequences: false).map(String.init) for segment in href.split(separator: "/") { - if segment == ".." { if !parts.isEmpty { parts.removeLast() } } else if segment != "." { parts.append(String(segment)) } + if segment == ".." { + if !parts.isEmpty { + parts.removeLast() + } + } else if segment != "." { + parts.append(String(segment)) + } } return parts.joined(separator: "/") } diff --git a/Tests/SchemaXSTSRegressionTests.swift b/Tests/SchemaXSTSRegressionTests.swift index b322edc..aab525b 100644 --- a/Tests/SchemaXSTSRegressionTests.swift +++ b/Tests/SchemaXSTSRegressionTests.swift @@ -97,8 +97,12 @@ struct SchemaXSTSRegressionTests { let addXSD = try String(contentsOfFile: "\(base)/address.xsd", encoding: .utf8) let itemXSD = try String(contentsOfFile: "\(base)/itematt.xsd", encoding: .utf8) let loader: (String) -> String? = { loc in - if loc.hasSuffix("address.xsd") { return addXSD } - if loc.hasSuffix("itematt.xsd") { return itemXSD } + if loc.hasSuffix("address.xsd") { + return addXSD + } + if loc.hasSuffix("itematt.xsd") { + return itemXSD + } return nil } let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader) @@ -114,8 +118,12 @@ struct SchemaXSTSRegressionTests { let addXSD = try String(contentsOfFile: "\(base)/address.xsd", encoding: .utf8) let itemXSD = try String(contentsOfFile: "\(base)/itematt.xsd", encoding: .utf8) let loader: (String) -> String? = { loc in - if loc.hasSuffix("address.xsd") { return addXSD } - if loc.hasSuffix("itematt.xsd") { return itemXSD } + if loc.hasSuffix("address.xsd") { + return addXSD + } + if loc.hasSuffix("itematt.xsd") { + return itemXSD + } return nil } let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader) diff --git a/Tests/SiblingAxisTests.swift b/Tests/SiblingAxisTests.swift index ebb71fd..aeafe35 100644 --- a/Tests/SiblingAxisTests.swift +++ b/Tests/SiblingAxisTests.swift @@ -13,7 +13,9 @@ struct SiblingAxisTests { private func names(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.doc)).compactMap { - if case let .node(node) = $0 { return node.element?.name.localName } + if case let .node(node) = $0 { + return node.element?.name.localName + } return nil } } diff --git a/Tests/ValidationConfigurationTests.swift b/Tests/ValidationConfigurationTests.swift index 712ec3d..e4cc65e 100644 --- a/Tests/ValidationConfigurationTests.swift +++ b/Tests/ValidationConfigurationTests.swift @@ -64,10 +64,40 @@ struct ValidationConfigurationTests { func test_xsdInstance() { let validator = PureXML.Validation.XSD.validator() #expect(validator.nonReferenceValidationDescriptions == [ - "The document element is valid against its XSD type", + "An element carries xsi:nil only when it is declared nillable", + "A nilled element has no content", + "A nilled element carries no fixed value constraint", + "A fixed element has no element children", + "A fixed element's content equals its fixed value", + "An empty-content element has no element children", + "A simple-content element has no element children", + "A simple-content element's text is valid against its type", + "An element-only or empty-content element carries no text", + "A strict wildcard-matched element has a global declaration", + "A simple-typed element carries only schema-instance and namespace attributes", + "A simple-typed element has no element children", + "A simple-typed element's value is valid against its type", + "A type reference resolves to a known, non-circular type", + "An element's children conform to its content model", + "A declared attribute's value is valid against its type", + "A declared attribute's value equals its fixed value", + "A required attribute is present", + "An element carries at most one attribute of type ID", + "An attribute is declared or permitted by a wildcard", + "A strict wildcard-matched attribute has a global declaration", + "A wildcard-matched attribute's value is valid against its type", + "A wildcard-matched attribute's value equals its fixed value", + "An xsi:type substitution is validly derived and not blocked", + "An xsi:type override names a known, validly derived type", + "Every xs:ID is unique and every xs:IDREF resolves to one", ]) #expect(validator.referenceValidationDescriptions == [ - "XSD identity constraints hold", + "An identity constraint's selector and field XPaths compile", + "A key constraint's fields are all present", + "A key or unique constraint's values are distinct", + "A keyref constraint's value matches a key or unique in scope", + "An identity constraint field selects at most one node", + "An identity constraint field identifies a node with simple content", ]) } @@ -125,6 +155,6 @@ struct ValidationConfigurationTests { func test_builtinRegistry() { let descriptions = PureXML.Validation.BuiltinValidation.allDescriptions #expect(descriptions.count == Set(descriptions).count) - #expect(descriptions.count == 24) + #expect(descriptions.count == 55) } } diff --git a/Tests/ValidationFieldCoverageTests.swift b/Tests/ValidationFieldCoverageTests.swift index 595e778..a09ef0c 100644 --- a/Tests/ValidationFieldCoverageTests.swift +++ b/Tests/ValidationFieldCoverageTests.swift @@ -42,9 +42,9 @@ struct ValidationFieldCoverageTests { "dtdUndeclaredElement", "dtdUndeclaredAttributes", "dtdDeclarationValidity", "dtdRootElementType", "dtdIdentifierIntegrity", "dtdStandaloneAttributes", "dtdStandaloneElementWhitespace", "dtdParseAdvisories", "htmlVoidElementsAreEmpty", "htmlRequiredParent", - "htmlUniqueIdentifiers", "xsdContentValidity", "xsdIdentityConstraints", + "htmlUniqueIdentifiers", "xsdFinalRespected", "xsdRestrictionsAreSubsets", "conformanceMatchesExpected", - "xsdStreamingShallowValidity", + "xsdStreamingShallowValidity", "xsdStreamingIdConsistency", ]) return tokens }() @@ -75,7 +75,9 @@ struct ValidationFieldCoverageTests { var currentModel: String? for line in text.split(separator: "\n", omittingEmptySubsequences: false) { let trimmed = line.trimmingCharacters(in: .whitespaces) - if trimmed.isEmpty || trimmed.hasPrefix("#") { continue } + if trimmed.isEmpty || trimmed.hasPrefix("#") { + continue + } let parts = trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init) guard let keyword = parts.first else { continue } applyRegistryLine(keyword, parts: parts, model: ¤tModel, registry: ®istry) diff --git a/Tests/ValidationFrameworkTests.swift b/Tests/ValidationFrameworkTests.swift index 50e8d0c..0ef1fb6 100644 --- a/Tests/ValidationFrameworkTests.swift +++ b/Tests/ValidationFrameworkTests.swift @@ -175,6 +175,28 @@ struct ValidationFrameworkTests { #expect(rule(missing).count == 1) } + @Test("unwrapAndLookup skips a nil reference, errors on an unresolved one, else validates the resolved value") + func test_unwrapAndLookup() { + struct Item: PureXML.Validation.Validatable { var ref: String? } + struct Target: PureXML.Validation.Validatable { var key: String } + struct Store { var targets: [String: Target] = [:] } + typealias Context = PureXML.Validation.ValidationContext + let rule: (Context) -> [PureXML.Validation.ValidationError] = unwrapAndLookup( + \Store.targets, + name: \Item.ref, + missing: { "missing \($0)" }, + into: PureXML.Validation.Validation(description: "present", check: { _ in true }), + ) + let store = Store(targets: ["a": Target(key: "a")]) + let onNil = Context(document: store, subject: Item(ref: nil), codingPath: []) + #expect(rule(onNil).isEmpty) + let onMissing = Context(document: store, subject: Item(ref: "z"), codingPath: []) + #expect(rule(onMissing).count == 1) + #expect(rule(onMissing).first?.reason == "missing z") + let onResolved = Context(document: store, subject: Item(ref: "a"), codingPath: []) + #expect(rule(onResolved).isEmpty) + } + @Test("Parameter-pack KeyPath validating mixes void-document builtins") func test_keyPathParameterPack() { let validator = PureXML.Validation.Validator.blank diff --git a/Tests/ValidationRuleRegistryTests.swift b/Tests/ValidationRuleRegistryTests.swift index 05acaa9..4f9079f 100644 --- a/Tests/ValidationRuleRegistryTests.swift +++ b/Tests/ValidationRuleRegistryTests.swift @@ -28,19 +28,50 @@ struct ValidationRuleRegistryTests { "htmlVoidElementsAreEmpty": ("test_htmlVoidElementsAreEmpty", "test_htmlVoidElementsAreEmpty_succeeds"), "htmlRequiredParent": ("test_htmlRequiredParent", "test_htmlRequiredParent_succeeds"), "htmlUniqueIdentifiers": ("test_htmlUniqueIdentifiers", "test_htmlUniqueIdentifiers_succeeds"), - "xsdContentValidity": ("test_xsdContentValidity", "test_xsdContentValidity_succeeds"), - "xsdIdentityConstraints": ("test_xsdIdentityConstraints", "test_xsdIdentityConstraints_succeeds"), + "xsdNilAttributeRequiresNillable": ("test_nilAttributeRequiresNillable", "test_nilAttributeRequiresNillable_succeeds"), + "xsdNilElementMustBeEmpty": ("test_nilElementMustBeEmpty", "test_nilElementMustBeEmpty_succeeds"), + "xsdNilElementNoFixedConstraint": ("test_nilElementNoFixedConstraint", "test_nilElementNoFixedConstraint_succeeds"), + "xsdFixedElementNoChildren": ("test_fixedElementNoChildren", "test_fixedElementNoChildren_succeeds"), + "xsdFixedElementValueMatches": ("test_fixedElementValueMatches", "test_fixedElementValueMatches_succeeds"), + "xsdEmptyContentForbidsChildren": ("test_emptyContentForbidsChildren", "test_emptyContentForbidsChildren_succeeds"), + "xsdSimpleContentForbidsChildren": ("test_simpleContentForbidsChildren", "test_simpleContentForbidsChildren_succeeds"), + "xsdSimpleContentTypeValid": ("test_simpleContentTypeValid", "test_simpleContentTypeValid_succeeds"), + "xsdTextForbidden": ("test_textForbidden", "test_textForbidden_succeeds"), + "xsdWildcardElementUndeclared": ("test_wildcardElementUndeclared", "test_wildcardElementUndeclared_succeeds"), + "xsdSimpleTypeDisallowedAttribute": ("test_simpleTypeDisallowedAttribute", "test_simpleTypeDisallowedAttribute_succeeds"), + "xsdSimpleTypeForbidsChildren": ("test_simpleTypeForbidsChildren", "test_simpleTypeForbidsChildren_succeeds"), + "xsdSimpleTypeValueValid": ("test_simpleTypeValueValid", "test_simpleTypeValueValid_succeeds"), + "xsdTypeReferenceResolves": ("test_typeReferenceResolves", "test_typeReferenceResolves_succeeds"), + "xsdContentModelStructure": ("test_contentModelStructure", "test_contentModelStructure_succeeds"), + "xsdAttributeTypeValid": ("test_attributeTypeValid", "test_attributeTypeValid_succeeds"), + "xsdAttributeFixedMatches": ("test_attributeFixedMatches", "test_attributeFixedMatches_succeeds"), + "xsdAttributeRequiredPresent": ("test_attributeRequiredPresent", "test_attributeRequiredPresent_succeeds"), + "xsdAttributeSingleID": ("test_attributeSingleID", "test_attributeSingleID_succeeds"), + "xsdAttributeDeclaredOrPermitted": ("test_attributeDeclaredOrPermitted", "test_attributeDeclaredOrPermitted_succeeds"), + "xsdWildcardAttributeUndeclared": ("test_wildcardAttributeUndeclared", "test_wildcardAttributeUndeclared_succeeds"), + "xsdWildcardAttributeTypeValid": ("test_wildcardAttributeTypeValid", "test_wildcardAttributeTypeValid_succeeds"), + "xsdWildcardAttributeFixedMatches": ("test_wildcardAttributeFixedMatches", "test_wildcardAttributeFixedMatches_succeeds"), + "xsdXsiTypeSubstitutionValid": ("test_xsiTypeSubstitutionValid", "test_xsiTypeSubstitutionValid_succeeds"), + "xsdXsiTypeOverrideResolves": ("test_xsiTypeOverrideResolves", "test_xsiTypeOverrideResolves_succeeds"), + "xsdIdIdrefConsistent": ("test_idIdrefConsistent", "test_idIdrefConsistent_succeeds"), + "xsdIdentityConstraintXPathsValid": ("test_identityConstraintXPathsValid", "test_identityConstraintXPathsValid_succeeds"), + "xsdIdentityKeyFieldPresent": ("test_identityKeyFieldPresent", "test_identityKeyFieldPresent_succeeds"), + "xsdIdentityValueUnique": ("test_identityValueUnique", "test_identityValueUnique_succeeds"), + "xsdIdentityKeyrefResolves": ("test_identityKeyrefResolves", "test_identityKeyrefResolves_succeeds"), + "xsdIdentityFieldCardinality": ("test_identityFieldCardinality", "test_identityFieldCardinality_succeeds"), + "xsdIdentityFieldMustBeSimple": ("test_identityFieldMustBeSimple", "test_identityFieldMustBeSimple_succeeds"), "xsdFinalRespected": ("test_xsdFinalRespected", "test_xsdFinalRespected_succeeds"), "xsdRestrictionsAreSubsets": ("test_xsdRestrictionsAreSubsets", "test_xsdRestrictionsAreSubsets_succeeds"), "conformanceMatchesExpected": ("test_conformanceMatchesExpected", "test_conformanceMatchesExpected_succeeds"), "xsdStreamingShallowValidity": ("test_xsdStreamingShallowValidity", "test_xsdStreamingShallowValidity_succeeds"), + "xsdStreamingIdConsistency": ("test_xsdStreamingIdConsistency", "test_xsdStreamingIdConsistency_succeeds"), ] @Test("Every builtin rule id has a fail and succeed test registered") func test_allRulesPaired() { let ids = PureXML.Validation.BuiltinValidation.allRuleIDs #expect(Set(Self.pairs.keys) == Set(ids), "pairs keys must match allRuleIDs") - #expect(ids.count == 24) + #expect(ids.count == 55) } @Test("Builtin descriptions stay unique") diff --git a/Tests/ValidationRuleTestSupport.swift b/Tests/ValidationRuleTestSupport.swift index 8fb5cdc..ee3f7af 100644 --- a/Tests/ValidationRuleTestSupport.swift +++ b/Tests/ValidationRuleTestSupport.swift @@ -1,9 +1,50 @@ +import Testing @testable import PureXML enum ValidationRuleTestSupport { typealias DTDSchema = PureXML.Validation.DTDSchema typealias XSDContext = PureXML.Validation.XSDContext + /// `xmlns:xsi` namespace declaration, spliced into instance fixtures that need + /// `xsi:nil`/`xsi:type`. + static let xsiNS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" + + /// Compiles `schema`, parses `instance`, and runs `rule` alone (isolated via + /// `.blank.validating(rule)`) against the root element declared `rootName`. + /// Shared by every `ValidationRuleXSD*Tests` suite. + static func xsdErrors( + _ rule: PureXML.Validation.Validation, + schema: String, + instance: String, + rootName: String, + ) throws -> [PureXML.Validation.ValidationError] { + let compiled = try PureXML.Schema.XSDParser.parse(schema) + let node = try PureXML.parse(instance) + guard case let .document(children) = node, let root = children.compactMap(\.element).first else { + Issue.record("no root element") + return [] + } + let context = XSDContext( + types: compiled.types, + constraints: compiled.constraints, + rootDeclaration: compiled.elements[rootName], + nillableElements: compiled.nillableElements, + elementConstraints: compiled.elementConstraints, + abstractTypes: compiled.nsAbstractTypes, + typeBlock: compiled.nsTypeBlock, + elementBlock: compiled.nsElementBlock, + typeDerivation: compiled.nsTypeDerivation, + unionMembers: compiled.nsUnionMembers, + globalAttributes: compiled.globalAttributes, + identityFieldTypes: compiled.identityFieldTypes, + identityFieldConstraints: compiled.identityFieldConstraints, + identityNonSimpleFields: compiled.identityNonSimpleFields, + ) + return PureXML.Validation.Validator.blank + .validating(rule) + .errors(for: .element(root), in: context) + } + static func dtd(_ xml: String, resolver: PureXML.Parsing.EntityResolver = .refusing) throws -> (PureXML.Model.Node, DTDSchema) { let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: .init(allowDoctype: true), resolver: resolver) let standalone = parsed.declaration?.standalone == true diff --git a/Tests/ValidationRuleXSDAttributeTests.swift b/Tests/ValidationRuleXSDAttributeTests.swift new file mode 100644 index 0000000..b984aed --- /dev/null +++ b/Tests/ValidationRuleXSDAttributeTests.swift @@ -0,0 +1,205 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): Validation rules XSD: attributes. +@Suite("Validation rules XSD: attributes") +struct ValidationRuleXSDAttributeTests { + // MARK: attributeTypeValid / attributeFixedMatches / attributeRequiredPresent / attributeSingleID + + private let requiredIntAttributeSchema = """ + + + + """ + + @Test("attributeTypeValid rejects an attribute value invalid against its type") + func test_attributeTypeValid() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeTypeValid, schema: requiredIntAttributeSchema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("attribute 'a'") == true) + } + + @Test("attributeTypeValid accepts a valid attribute value") + func test_attributeTypeValid_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeTypeValid, schema: requiredIntAttributeSchema, instance: "", rootName: "n") + #expect(errors.isEmpty) + } + + @Test("attributeRequiredPresent rejects a missing required attribute") + func test_attributeRequiredPresent() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeRequiredPresent, schema: requiredIntAttributeSchema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("missing required attribute") == true) + } + + @Test("attributeRequiredPresent accepts a present required attribute") + func test_attributeRequiredPresent_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.attributeRequiredPresent, + schema: requiredIntAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + private let fixedAttributeSchema = """ + + + + """ + + @Test("attributeFixedMatches rejects a value that differs from the fixed value") + func test_attributeFixedMatches() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeFixedMatches, schema: fixedAttributeSchema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("is fixed and must be") == true) + } + + @Test("attributeFixedMatches accepts a value equal to the fixed value") + func test_attributeFixedMatches_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeFixedMatches, schema: fixedAttributeSchema, instance: "", rootName: "n") + #expect(errors.isEmpty) + } + + private let twoIDAttributesSchema = """ + + + + """ + + @Test("attributeSingleID rejects more than one attribute of type ID") + func test_attributeSingleID() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.attributeSingleID, + schema: twoIDAttributesSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("at most one attribute of type ID") == true) + } + + @Test("attributeSingleID accepts a single attribute of type ID") + func test_attributeSingleID_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeSingleID, schema: twoIDAttributesSchema, instance: "", rootName: "n") + #expect(errors.isEmpty) + } + + // MARK: attributeDeclaredOrPermitted + + private let emptyContentSchema = """ + + + + """ + + @Test("attributeDeclaredOrPermitted rejects an undeclared attribute with no wildcard") + func test_attributeDeclaredOrPermitted() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.attributeDeclaredOrPermitted, + schema: emptyContentSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("undeclared attribute") == true) + } + + @Test("attributeDeclaredOrPermitted accepts no undeclared attributes") + func test_attributeDeclaredOrPermitted_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.attributeDeclaredOrPermitted, schema: emptyContentSchema, instance: "", rootName: "n") + #expect(errors.isEmpty) + } + + // MARK: wildcardAttributeUndeclared / wildcardAttributeTypeValid / wildcardAttributeFixedMatches + + private let strictWildcardAttributeSchema = """ + + + + """ + + @Test("wildcardAttributeUndeclared rejects a strict wildcard match with no global declaration") + func test_wildcardAttributeUndeclared() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeUndeclared, + schema: strictWildcardAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("no declaration for wildcard-matched attribute") == true) + } + + @Test("wildcardAttributeUndeclared accepts no wildcard-matched attributes") + func test_wildcardAttributeUndeclared_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeUndeclared, + schema: strictWildcardAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + private let laxWildcardIntAttributeSchema = """ + + + + + """ + + @Test("wildcardAttributeTypeValid rejects a wildcard-matched attribute invalid against its type") + func test_wildcardAttributeTypeValid() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeTypeValid, + schema: laxWildcardIntAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("attribute 'foo'") == true) + } + + @Test("wildcardAttributeTypeValid accepts a valid wildcard-matched attribute") + func test_wildcardAttributeTypeValid_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeTypeValid, + schema: laxWildcardIntAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + private let laxWildcardFixedAttributeSchema = """ + + + + + """ + + @Test("wildcardAttributeFixedMatches rejects a wildcard-matched attribute that differs from its fixed value") + func test_wildcardAttributeFixedMatches() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeFixedMatches, + schema: laxWildcardFixedAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("is fixed and must be") == true) + } + + @Test("wildcardAttributeFixedMatches accepts a wildcard-matched attribute equal to its fixed value") + func test_wildcardAttributeFixedMatches_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardAttributeFixedMatches, + schema: laxWildcardFixedAttributeSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDContentTests.swift b/Tests/ValidationRuleXSDContentTests.swift new file mode 100644 index 0000000..9659677 --- /dev/null +++ b/Tests/ValidationRuleXSDContentTests.swift @@ -0,0 +1,159 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): content and wildcard rules. +@Suite("Validation rules XSD: content and wildcard rules") +struct ValidationRuleXSDContentTests { + // MARK: emptyContentForbidsChildren + + private let emptyContentSchema = """ + + + + """ + + @Test("emptyContentForbidsChildren rejects children on an empty-content element") + func test_emptyContentForbidsChildren() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.emptyContentForbidsChildren, + schema: emptyContentSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason == "element must be empty") + } + + @Test("emptyContentForbidsChildren accepts a truly empty element") + func test_emptyContentForbidsChildren_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.emptyContentForbidsChildren, + schema: emptyContentSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: simpleContentForbidsChildren / simpleContentTypeValid + + private let simpleContentSchema = """ + + + + + + """ + + @Test("simpleContentForbidsChildren rejects element children under simpleContent") + func test_simpleContentForbidsChildren() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleContentForbidsChildren, + schema: simpleContentSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason == "element must not have children") + } + + @Test("simpleContentForbidsChildren accepts text-only simpleContent") + func test_simpleContentForbidsChildren_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleContentForbidsChildren, + schema: simpleContentSchema, + instance: "5", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + @Test("simpleContentTypeValid rejects text invalid against the simpleContent type") + func test_simpleContentTypeValid() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleContentTypeValid, + schema: simpleContentSchema, + instance: "x", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.hasPrefix("content:") == true) + } + + @Test("simpleContentTypeValid accepts text valid against the simpleContent type") + func test_simpleContentTypeValid_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleContentTypeValid, + schema: simpleContentSchema, + instance: "5", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: textForbidden + + private let elementOnlyOptionalSchema = """ + + + + + + """ + + @Test("textForbidden rejects text under element-only content") + func test_textForbidden() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.textForbidden, + schema: elementOnlyOptionalSchema, + instance: "hello", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason == "element must not contain text") + } + + @Test("textForbidden accepts element-only content with no text") + func test_textForbidden_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.textForbidden, + schema: elementOnlyOptionalSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: wildcardElementUndeclared + + private let strictWildcardElementSchema = """ + + + + + + """ + + @Test("wildcardElementUndeclared rejects a strict wildcard match with no global declaration") + func test_wildcardElementUndeclared() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardElementUndeclared, + schema: strictWildcardElementSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("no declaration for wildcard-matched element") == true) + } + + @Test("wildcardElementUndeclared accepts an optional wildcard with no match") + func test_wildcardElementUndeclared_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.wildcardElementUndeclared, + schema: strictWildcardElementSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDDerivationTests.swift b/Tests/ValidationRuleXSDDerivationTests.swift new file mode 100644 index 0000000..99f4c50 --- /dev/null +++ b/Tests/ValidationRuleXSDDerivationTests.swift @@ -0,0 +1,121 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): Validation rules XSD: derivation and identifiers. +@Suite("Validation rules XSD: derivation and identifiers") +struct ValidationRuleXSDDerivationTests { + // MARK: xsiTypeSubstitutionValid / xsiTypeOverrideResolves + + private let abstractDerivedSchema = """ + + + + + + """ + + @Test("xsiTypeSubstitutionValid rejects an abstract declared type with no xsi:type override") + func test_xsiTypeSubstitutionValid() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.xsiTypeSubstitutionValid, schema: abstractDerivedSchema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("abstract type") == true) + } + + @Test("xsiTypeSubstitutionValid accepts an xsi:type naming a concrete derived type") + func test_xsiTypeSubstitutionValid_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.xsiTypeSubstitutionValid, + schema: abstractDerivedSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + private let simpleStringSchema = """ + + + + """ + + @Test("xsiTypeOverrideResolves rejects an xsi:type naming an unknown type") + func test_xsiTypeOverrideResolves() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.xsiTypeOverrideResolves, + schema: simpleStringSchema, + instance: "text", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("unknown xsi:type") == true) + } + + @Test("xsiTypeOverrideResolves accepts an xsi:type naming a resolvable type") + func test_xsiTypeOverrideResolves_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.xsiTypeOverrideResolves, + schema: simpleStringSchema, + instance: "text", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: idIdrefConsistent + + private let idElementSchema = """ + + + + + + + + """ + + @Test("idIdrefConsistent rejects a duplicate xs:ID value") + func test_idIdrefConsistent() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.idIdrefConsistent, + schema: idElementSchema, + instance: "", + rootName: "n", + ) + #expect(!errors.isEmpty) + } + + @Test("idIdrefConsistent accepts distinct xs:ID values") + func test_idIdrefConsistent_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.idIdrefConsistent, + schema: idElementSchema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: xsdStreamingIdConsistency + + @Test("xsdStreamingIdConsistency rejects a duplicate xs:ID recorded during streaming") + func test_xsdStreamingIdConsistency() { + let validator = PureXML.Schema.ComplexValidator() + validator.recordIDs(PureXML.Schema.SimpleType(base: .id), value: "x1", at: []) + validator.recordIDs(PureXML.Schema.SimpleType(base: .id), value: "x1", at: []) + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Schema.ComplexValidator.idConsistency) + .errors(for: validator, in: ()) + #expect(!errors.isEmpty) + } + + @Test("xsdStreamingIdConsistency accepts distinct xs:ID values recorded during streaming") + func test_xsdStreamingIdConsistency_succeeds() { + let validator = PureXML.Schema.ComplexValidator() + validator.recordIDs(PureXML.Schema.SimpleType(base: .id), value: "x1", at: []) + validator.recordIDs(PureXML.Schema.SimpleType(base: .id), value: "x2", at: []) + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Schema.ComplexValidator.idConsistency) + .errors(for: validator, in: ()) + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDIdentityTests.swift b/Tests/ValidationRuleXSDIdentityTests.swift new file mode 100644 index 0000000..3eeee2b --- /dev/null +++ b/Tests/ValidationRuleXSDIdentityTests.swift @@ -0,0 +1,260 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): Validation rules XSD: identity constraints. +@Suite("Validation rules XSD: identity constraints") +struct ValidationRuleXSDIdentityTests { + // MARK: identityConstraintXPathsValid + + private let uniqueListSchemaTemplate = """ + + + + + + + + + """ + + @Test("identityConstraintXPathsValid rejects a malformed selector XPath") + func test_identityConstraintXPathsValid() throws { + let schema = String(format: uniqueListSchemaTemplate, "[[[") + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityConstraintXPathsValid, + schema: schema, + instance: "", + rootName: "list", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("invalid selector XPath") == true) + } + + @Test("identityConstraintXPathsValid accepts a well-formed selector XPath") + func test_identityConstraintXPathsValid_succeeds() throws { + let schema = String(format: uniqueListSchemaTemplate, "item") + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityConstraintXPathsValid, + schema: schema, + instance: "", + rootName: "list", + ) + #expect(errors.isEmpty) + } + + // MARK: identityKeyFieldPresent + + private let keyListSchema = """ + + + + + + + + + """ + + @Test("identityKeyFieldPresent rejects a key target missing its field") + func test_identityKeyFieldPresent() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityKeyFieldPresent, + schema: keyListSchema, + instance: "", + rootName: "list", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("a field is missing") == true) + } + + @Test("identityKeyFieldPresent accepts a key target with its field present") + func test_identityKeyFieldPresent_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityKeyFieldPresent, + schema: keyListSchema, + instance: "", + rootName: "list", + ) + #expect(errors.isEmpty) + } + + // MARK: identityValueUnique + + private let uniqueListSchema = """ + + + + + + + + + """ + + @Test("identityValueUnique reports a duplicate key field") + func test_identityValueUnique() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityValueUnique, + schema: uniqueListSchema, + instance: "", + rootName: "list", + ) + #expect(!errors.isEmpty) + } + + @Test("identityValueUnique accepts distinct field values") + func test_identityValueUnique_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityValueUnique, + schema: uniqueListSchema, + instance: "", + rootName: "list", + ) + #expect(errors.isEmpty) + } + + // MARK: identityKeyrefResolves + + private let keyrefSchema = """ + + + + + + + + + + + + + + + + + + + + + + + + + """ + + @Test("identityKeyrefResolves rejects a keyref value with no matching key") + func test_identityKeyrefResolves() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityKeyrefResolves, + schema: keyrefSchema, + instance: "", + rootName: "root", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("no matching key") == true) + } + + @Test("identityKeyrefResolves accepts a keyref value that matches a key") + func test_identityKeyrefResolves_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityKeyrefResolves, + schema: keyrefSchema, + instance: "", + rootName: "root", + ) + #expect(errors.isEmpty) + } + + // MARK: identityFieldCardinality + + private let multiChildFieldSchema = """ + + + + + + + + + + + """ + + @Test("identityFieldCardinality rejects a field that selects more than one node") + func test_identityFieldCardinality() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityFieldCardinality, + schema: multiChildFieldSchema, + instance: "ab", + rootName: "list", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("must select at most one node") == true) + } + + @Test("identityFieldCardinality accepts a field that selects exactly one node") + func test_identityFieldCardinality_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityFieldCardinality, + schema: multiChildFieldSchema, + instance: "a", + rootName: "list", + ) + #expect(errors.isEmpty) + } + + // MARK: identityFieldMustBeSimple + + private let complexChildFieldSchema = """ + + + + + + + + + + + + + + + """ + + private let simpleChildFieldSchema = """ + + + + + + + + + + + """ + + @Test("identityFieldMustBeSimple rejects a field identifying a node with element children") + func test_identityFieldMustBeSimple() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityFieldMustBeSimple, + schema: complexChildFieldSchema, + instance: "x", + rootName: "list", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("must identify a node with simple content") == true) + } + + @Test("identityFieldMustBeSimple accepts a field identifying a simple-content node") + func test_identityFieldMustBeSimple_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.identityFieldMustBeSimple, + schema: simpleChildFieldSchema, + instance: "a", + rootName: "list", + ) + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDNilFixedTests.swift b/Tests/ValidationRuleXSDNilFixedTests.swift new file mode 100644 index 0000000..5f254b7 --- /dev/null +++ b/Tests/ValidationRuleXSDNilFixedTests.swift @@ -0,0 +1,165 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): nillable and fixed-value rules. +@Suite("Validation rules XSD: nillable and fixed-value rules") +struct ValidationRuleXSDNilFixedTests { + // MARK: nilAttributeRequiresNillable + + @Test("nilAttributeRequiresNillable rejects xsi:nil on a non-nillable element") + func test_nilAttributeRequiresNillable() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilAttributeRequiresNillable, + schema: schema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("not nillable") == true) + } + + @Test("nilAttributeRequiresNillable accepts xsi:nil on a nillable element") + func test_nilAttributeRequiresNillable_succeeds() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilAttributeRequiresNillable, + schema: schema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: nilElementMustBeEmpty + + @Test("nilElementMustBeEmpty rejects content on a nilled element") + func test_nilElementMustBeEmpty() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilElementMustBeEmpty, + schema: schema, + instance: "x", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("must be empty") == true) + } + + @Test("nilElementMustBeEmpty accepts an empty nilled element") + func test_nilElementMustBeEmpty_succeeds() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilElementMustBeEmpty, + schema: schema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: nilElementNoFixedConstraint + + @Test("nilElementNoFixedConstraint rejects a nilled element declared fixed") + func test_nilElementNoFixedConstraint() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilElementNoFixedConstraint, + schema: schema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("fixed value constraint") == true) + } + + @Test("nilElementNoFixedConstraint accepts a nilled element with no fixed constraint") + func test_nilElementNoFixedConstraint_succeeds() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.nilElementNoFixedConstraint, + schema: schema, + instance: "", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + // MARK: fixedElementNoChildren / fixedElementValueMatches + + private let fixedStringSchema = """ + + + + """ + + @Test("fixedElementNoChildren rejects element children on a fixed element") + func test_fixedElementNoChildren() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.fixedElementNoChildren, + schema: fixedStringSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("must not have element children") == true) + } + + @Test("fixedElementNoChildren accepts a fixed element with only text") + func test_fixedElementNoChildren_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.fixedElementNoChildren, + schema: fixedStringSchema, + instance: "5", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + @Test("fixedElementValueMatches rejects text that differs from the fixed value") + func test_fixedElementValueMatches() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.fixedElementValueMatches, + schema: fixedStringSchema, + instance: "6", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("is fixed and must be") == true) + } + + @Test("fixedElementValueMatches accepts text equal to the fixed value") + func test_fixedElementValueMatches_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.fixedElementValueMatches, + schema: fixedStringSchema, + instance: "5", + rootName: "n", + ) + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDStructureTests.swift b/Tests/ValidationRuleXSDStructureTests.swift new file mode 100644 index 0000000..b67ff96 --- /dev/null +++ b/Tests/ValidationRuleXSDStructureTests.swift @@ -0,0 +1,133 @@ +import Testing +@testable import PureXML + +/// Per-rule isolation tests (Validation rules XSD): Validation rules XSD: simple type and structure. +@Suite("Validation rules XSD: simple type and structure") +struct ValidationRuleXSDStructureTests { + // MARK: simpleTypeDisallowedAttribute / simpleTypeForbidsChildren / simpleTypeValueValid + + private let simpleStringSchema = """ + + + + """ + + @Test("simpleTypeDisallowedAttribute rejects an attribute on a simple-typed element") + func test_simpleTypeDisallowedAttribute() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleTypeDisallowedAttribute, + schema: simpleStringSchema, + instance: "text", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("has a simple type") == true) + } + + @Test("simpleTypeDisallowedAttribute accepts a simple-typed element with no attributes") + func test_simpleTypeDisallowedAttribute_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleTypeDisallowedAttribute, + schema: simpleStringSchema, + instance: "text", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + @Test("simpleTypeForbidsChildren rejects element children on a simple-typed element") + func test_simpleTypeForbidsChildren() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleTypeForbidsChildren, + schema: simpleStringSchema, + instance: "", + rootName: "n", + ) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("must not have children") == true) + } + + @Test("simpleTypeForbidsChildren accepts a simple-typed element with only text") + func test_simpleTypeForbidsChildren_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors( + PureXML.Validation.XSD.simpleTypeForbidsChildren, + schema: simpleStringSchema, + instance: "text", + rootName: "n", + ) + #expect(errors.isEmpty) + } + + @Test("simpleTypeValueValid locates a simple-type violation at the root element") + func test_simpleTypeValueValid() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.simpleTypeValueValid, schema: schema, instance: "x", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("n") == true) + #expect(errors.first.map(ValidationRuleTestSupport.path) == ["n"]) + } + + @Test("simpleTypeValueValid accepts a valid integer at the boundary") + func test_simpleTypeValueValid_succeeds() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.simpleTypeValueValid, schema: schema, instance: "0", rootName: "n") + #expect(errors.isEmpty) + } + + // MARK: typeReferenceResolves + + @Test("typeReferenceResolves rejects a reference to an undeclared type") + func test_typeReferenceResolves() throws { + let schema = """ + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.typeReferenceResolves, schema: schema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("unknown type") == true) + } + + @Test("typeReferenceResolves accepts a reference that resolves to a declared type") + func test_typeReferenceResolves_succeeds() throws { + let schema = """ + + + + + """ + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.typeReferenceResolves, schema: schema, instance: "", rootName: "n") + #expect(errors.isEmpty) + } + + // MARK: contentModelStructure + + private let sequenceSchema = """ + + + + + + """ + + @Test("contentModelStructure rejects a child the content model does not allow") + func test_contentModelStructure() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.contentModelStructure, schema: sequenceSchema, instance: "", rootName: "n") + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("is not allowed here") == true) + } + + @Test("contentModelStructure accepts children matching the content model") + func test_contentModelStructure_succeeds() throws { + let errors = try ValidationRuleTestSupport.xsdErrors(PureXML.Validation.XSD.contentModelStructure, schema: sequenceSchema, instance: "x", rootName: "n") + #expect(errors.isEmpty) + } +} diff --git a/Tests/ValidationRuleXSDTests.swift b/Tests/ValidationRuleXSDTests.swift index 23a97bc..1125580 100644 --- a/Tests/ValidationRuleXSDTests.swift +++ b/Tests/ValidationRuleXSDTests.swift @@ -1,102 +1,11 @@ import Testing @testable import PureXML -/// Per-rule isolation tests (Validation rules XSD). +/// Per-rule isolation tests (Validation rules XSD): the schema-consistency layer +/// (`PureXML.Validation.XSDSchema`), unrelated to instance validation. @Suite("Validation rules XSD") struct ValidationRuleXSDTests { - @Test("XSD.contentValidity locates a simple-type violation at the root element") - func test_xsdContentValidity() throws { - let xsd = """ - - - - """ - let compiled = try PureXML.Schema.XSDParser.parse(xsd) - let node = try PureXML.parse("x") - guard case let .document(children) = node, let root = children.compactMap(\.element).first else { - Issue.record("no root element") - return - } - let context = ValidationRuleTestSupport.XSDContext( - types: compiled.types, - constraints: compiled.constraints, - rootDeclaration: compiled.elements["n"], - ) - // The validator is rooted at the document element, as Schema.Document does. - let errors = PureXML.Validation.Validator.blank - .validating(PureXML.Validation.XSD.contentValidity) - .errors(for: .element(root), in: context) - #expect(errors.count == 1) - #expect(errors.first?.reason.contains("n") == true) - #expect(errors.first.map(ValidationRuleTestSupport.path) == ["n"]) - } - - @Test("XSD.contentValidity accepts a valid integer at the boundary") - func test_xsdContentValidity_succeeds() throws { - let xsd = """ - - - - """ - let compiled = try PureXML.Schema.XSDParser.parse(xsd) - let node = try PureXML.parse("0") - guard case let .document(children) = node, let root = children.compactMap(\.element).first else { - Issue.record("no root element") - return - } - let context = ValidationRuleTestSupport.XSDContext(types: compiled.types, constraints: compiled.constraints, rootDeclaration: compiled.elements["n"]) - let errors = PureXML.Validation.Validator.blank - .validating(PureXML.Validation.XSD.contentValidity) - .errors(for: .element(root), in: context) - #expect(errors.isEmpty) - } - - @Test("XSD.identityConstraints reports a duplicate key field") - func test_xsdIdentityConstraints() throws { - let xsd = """ - - - - - - - - - """ - let compiled = try PureXML.Schema.XSDParser.parse(xsd) - let node = try PureXML.parse("") - guard case let .document(children) = node, let root = children.compactMap(\.element).first else { - Issue.record("no root element") - return - } - let context = ValidationRuleTestSupport.XSDContext(types: compiled.types, constraints: compiled.constraints, rootDeclaration: compiled.elements["list"]) - let errors = PureXML.Validation.Validator.blank - .validating(PureXML.Validation.XSD.identityConstraints) - .errors(for: .element(root), in: context) - #expect(!errors.isEmpty) - } - - @Test("XSD.identityConstraints accepts distinct field values") - func test_xsdIdentityConstraints_succeeds() throws { - let xsd = """ - - - - - - - - - """ - let compiled = try PureXML.Schema.XSDParser.parse(xsd) - let node = try PureXML.parse("") - guard case let .document(children) = node, let root = children.compactMap(\.element).first else { return } - let context = ValidationRuleTestSupport.XSDContext(types: compiled.types, constraints: compiled.constraints, rootDeclaration: compiled.elements["list"]) - #expect(PureXML.Validation.Validator.blank.validating(PureXML.Validation.XSD.identityConstraints).errors( - for: .element(root), - in: context, - ).isEmpty) - } + // MARK: XSDSchema (unchanged, schema-consistency layer) @Test("XSDSchema.finalRespected rejects a derivation forbidden by final") func test_xsdFinalRespected() throws { diff --git a/Tests/XPathDescendantFusionTests.swift b/Tests/XPathDescendantFusionTests.swift index 2a68571..7bd2ad6 100644 --- a/Tests/XPathDescendantFusionTests.swift +++ b/Tests/XPathDescendantFusionTests.swift @@ -18,7 +18,9 @@ struct XPathDescendantFusionTests { private func ids(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.source)).compactMap { - if case let .node(node) = $0 { return node.element?.attributes.first { $0.name.localName == "id" }?.value } + if case let .node(node) = $0 { + return node.element?.attributes.first { $0.name.localName == "id" }?.value + } return nil } } diff --git a/Tests/XPathFusedAttributeTests.swift b/Tests/XPathFusedAttributeTests.swift index f6e2c15..28cd45b 100644 --- a/Tests/XPathFusedAttributeTests.swift +++ b/Tests/XPathFusedAttributeTests.swift @@ -13,7 +13,9 @@ struct XPathFusedAttributeTests { private func values(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.source)).compactMap { - if case let .attribute(attribute) = $0 { return attribute.value } + if case let .attribute(attribute) = $0 { + return attribute.value + } return nil } } diff --git a/Tests/XPathFusedDescendantTests.swift b/Tests/XPathFusedDescendantTests.swift index e53b49f..b3dd195 100644 --- a/Tests/XPathFusedDescendantTests.swift +++ b/Tests/XPathFusedDescendantTests.swift @@ -19,7 +19,9 @@ struct XPathFusedDescendantTests { private func ids(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.source)).compactMap { - if case let .node(node) = $0 { return node.element?.attributes.first { $0.name.localName == "id" }?.value } + if case let .node(node) = $0 { + return node.element?.attributes.first { $0.name.localName == "id" }?.value + } return nil } } diff --git a/Tests/XPathSingleStepOrderTests.swift b/Tests/XPathSingleStepOrderTests.swift index 8d06fe3..004001c 100644 --- a/Tests/XPathSingleStepOrderTests.swift +++ b/Tests/XPathSingleStepOrderTests.swift @@ -22,14 +22,18 @@ struct XPathSingleStepOrderTests { private func ids(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.source)).compactMap { - if case let .node(node) = $0 { return node.element?.attributes.first { $0.name.localName == "id" }?.value } + if case let .node(node) = $0 { + return node.element?.attributes.first { $0.name.localName == "id" }?.value + } return nil } } private func names(_ path: String) throws -> [String] { try PureXML.XPath.Query(path).evaluate(over: PureXML.parse(Self.source)).compactMap { - if case let .node(node) = $0 { return node.element?.name.localName } + if case let .node(node) = $0 { + return node.element?.name.localName + } return nil } } diff --git a/Tests/XSLTParityTests.swift b/Tests/XSLTParityTests.swift index bab1928..af229fd 100644 --- a/Tests/XSLTParityTests.swift +++ b/Tests/XSLTParityTests.swift @@ -12,7 +12,9 @@ struct XSLTParityTests { guard output.hasPrefix("") else { return output } var body = String(output[end.upperBound...]) - if body.hasPrefix("\n") { body.removeFirst() } + if body.hasPrefix("\n") { + body.removeFirst() + } return body } diff --git a/Tests/XSLTParserTests.swift b/Tests/XSLTParserTests.swift index ae731e5..a767ce9 100644 --- a/Tests/XSLTParserTests.swift +++ b/Tests/XSLTParserTests.swift @@ -66,8 +66,20 @@ struct XSLTParserTests { Issue.record("expected three parts, got \(parts.count)") return } - if case let .literal(text) = parts[0] { #expect(text == "id-") } else { Issue.record("part 0") } - if case let .expression(expr) = parts[1] { #expect(expr == "@n") } else { Issue.record("part 1") } - if case let .literal(text) = parts[2] { #expect(text == "-{x}") } else { Issue.record("part 2") } + if case let .literal(text) = parts[0] { + #expect(text == "id-") + } else { + Issue.record("part 0") + } + if case let .expression(expr) = parts[1] { + #expect(expr == "@n") + } else { + Issue.record("part 1") + } + if case let .literal(text) = parts[2] { + #expect(text == "-{x}") + } else { + Issue.record("part 2") + } } } diff --git a/Tests/XSLTTests.swift b/Tests/XSLTTests.swift index 7490c5a..ce406dc 100644 --- a/Tests/XSLTTests.swift +++ b/Tests/XSLTTests.swift @@ -12,7 +12,9 @@ struct XSLTTests { guard output.hasPrefix("") else { return output } var body = String(output[end.upperBound...]) - if body.hasPrefix("\n") { body.removeFirst() } + if body.hasPrefix("\n") { + body.removeFirst() + } return body } diff --git a/Tests/XSTSDifferentialTests.swift b/Tests/XSTSDifferentialTests.swift index 0f73644..2419722 100644 --- a/Tests/XSTSDifferentialTests.swift +++ b/Tests/XSTSDifferentialTests.swift @@ -127,7 +127,9 @@ struct XSTSDifferentialTests { let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() let output = String(decoding: data, as: UTF8.self) - if output.contains("Schemas parser error") || output.contains("failed to compile") { return false } + if output.contains("Schemas parser error") || output.contains("failed to compile") { + return false + } return true #endif } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 89bdfd5..b750aec 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -318,7 +318,9 @@ struct XSTSSuiteTests { found += elements(named: name, under: child) } case let .element(element): - if element.name.localName == name { found.append(element) } + if element.name.localName == name { + found.append(element) + } for child in element.children { found += elements(named: name, under: child) } @@ -362,7 +364,9 @@ struct XSTSSuiteTests { var parts = base.split(separator: "/", omittingEmptySubsequences: false).map(String.init) for segment in href.split(separator: "/") { if segment == ".." { - if !parts.isEmpty { parts.removeLast() } + if !parts.isEmpty { + parts.removeLast() + } } else if segment != "." { parts.append(String(segment)) } diff --git a/docs/validation-field-registry.txt b/docs/validation-field-registry.txt index a487b68..a4cb5c0 100644 --- a/docs/validation-field-registry.txt +++ b/docs/validation-field-registry.txt @@ -18,20 +18,20 @@ FIELD parseAdvisories dtdParseAdvisories IGNORE isEmpty derived summary; no independent constraint MODEL PureXML.Validation.XSDContext -FIELD types xsdContentValidity,xsdStreamingShallowValidity -FIELD constraints xsdIdentityConstraints -FIELD rootDeclaration xsdContentValidity -FIELD nillableElements xsdContentValidity -FIELD elementConstraints xsdContentValidity -FIELD abstractTypes xsdContentValidity -FIELD typeBlock xsdContentValidity -FIELD elementBlock xsdContentValidity -FIELD typeDerivation xsdContentValidity +FIELD types xsdTypeReferenceResolves,xsdContentModelStructure,xsdXsiTypeSubstitutionValid,xsdXsiTypeOverrideResolves,xsdStreamingShallowValidity +FIELD constraints xsdIdentityConstraintXPathsValid,xsdIdentityKeyFieldPresent,xsdIdentityValueUnique,xsdIdentityKeyrefResolves,xsdIdentityFieldCardinality,xsdIdentityFieldMustBeSimple +FIELD rootDeclaration xsdContentModelStructure +FIELD nillableElements xsdNilAttributeRequiresNillable +FIELD elementConstraints xsdNilElementNoFixedConstraint,xsdFixedElementNoChildren,xsdFixedElementValueMatches +FIELD abstractTypes xsdXsiTypeSubstitutionValid +FIELD typeBlock xsdXsiTypeSubstitutionValid +FIELD elementBlock xsdXsiTypeSubstitutionValid +FIELD typeDerivation xsdXsiTypeSubstitutionValid MODEL PureXML.Model.Element -FIELD name dtdContentModel,dtdUndeclaredElement,dtdUndeclaredAttributes,htmlRequiredParent,htmlVoidElementsAreEmpty,xsdContentValidity +FIELD name dtdContentModel,dtdUndeclaredElement,dtdUndeclaredAttributes,htmlRequiredParent,htmlVoidElementsAreEmpty,xsdContentModelStructure FIELD attributes uniqueAttributes,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredAttributes,dtdStandaloneAttributes -FIELD children dtdContentModel,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentValidity +FIELD children dtdContentModel,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentModelStructure IGNORE text derived from direct text and CDATA children MODEL PureXML.Model.Attribute @@ -39,8 +39,8 @@ FIELD name uniqueAttributes,dtdUndeclaredAttributes,dtdRequiredAttributes,dtdFix FIELD value dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdIdentifierIntegrity MODEL PureXML.Model.Node -CASE document dtdDeclarationValidity,dtdRootElementType,dtdIdentifierIntegrity,htmlUniqueIdentifiers,xsdContentValidity,xsdIdentityConstraints -CASE element uniqueAttributes,dtdContentModel,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredElement,dtdUndeclaredAttributes,dtdStandaloneAttributes,dtdStandaloneElementWhitespace,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentValidity +CASE document dtdDeclarationValidity,dtdRootElementType,dtdIdentifierIntegrity,htmlUniqueIdentifiers,xsdContentModelStructure,xsdIdentityConstraintXPathsValid +CASE element uniqueAttributes,dtdContentModel,dtdRequiredAttributes,dtdFixedAttributeValues,dtdEnumeratedAttributeValues,dtdTokenizedAttributeTypes,dtdNotationAttributes,dtdUndeclaredElement,dtdUndeclaredAttributes,dtdStandaloneAttributes,dtdStandaloneElementWhitespace,htmlVoidElementsAreEmpty,htmlRequiredParent,xsdContentModelStructure CASE text dtdContentModel CASE cdata dtdContentModel IGNORE comment no DTD or XSD constraints on comments